Overview
SonicWall Capture Labs threat research team has observed fileless .Net managed code injection in a native 64-bit process. Native code or unmanaged code refers to low-level compiled code such as C/C++. Managed code refers to code that is written to target .NET and will not work without the CLR (Microsoft .NET engine) runtime libraries. The injected code belongs to AgentTesla malware.
Technical Analysis
The initial infection vector is a Word document that the client received as an email attachment. Upon opening this document, it will ask the user to enable a VBA macro. If enabled, this VBA macro downloads a 64-bit executable from the internet and executes it.
The downloaded binary is a 64-bit, Rust-compiled binary. We are focusing on the techniques used by this binary to inject the malicious AgentTesla payload into its own process memory using CLR Hosting.
The following are details of the 64-bit downloaded executable file.
MD5 : 4521162D45EFC83FA76C4B5C0D405265
SHA256 : F00ED06A1D402ECF760EC92F3280EF6C09E76036854ABACADCAC9311706ED97D
URL from which 64-bit executable downloaded:
https[:]//New-Coder[.]cc/Users/signed_20240329011751156[.]exe
Disabling Event Tracing for Windows (ETW)
On execution of the Rust binary, it patches the “EtwEventWrite” API from NTDLL using the NtProtectVirtualMemory, WriteProcessMemory and FlushInstructionCache APIs.
Figure 1: After the malware patches the “EtwEventWrite” API
This 64-bit malware process downloads an encoded shellcode from the following URL which contains the AgenetTesla payload.
URL of the shellcode:
https[:]//New-Coder[.]cc/Users/shellcodeAny_20240329011339585[.]bin
Next, the malware starts the execution of the downloaded shellcode using the “EnumSystemLocalesA” API by passing the address of the shellcode to the API as the callback function argument.
Figure 2: Moved shellcode from read-write memory to executable memory and starts its execution
The shellcode parses PEB and PEB_LDR_DATA to resolve the API dynamically. It will resolve the VirtualAlloc, VirtualFree, and RtlExitUserProcess APIs using an API hashing technique.
Next, the shellcode allocates read-write memory using the “VirtualAlloc” API and moves 0x3E3C0 bytes from the shellcode to the allocated memory. These bytes are the encoded AgentTesla payload.
Figure 3: Moved shellcode data in read-write memory and starts decryption routine
As shown in Figure 3 above, the first 4bytes (DWORD) are the size of encoded data followed by encoded data.
Next, it proceeds to decrypt the payload. The shellcode uses a customized decryption routine where it performs single-byte XOR decryption in a loop, and for every iteration, it decrypts 0x10 bytes in the payload with a 0x10-byte encryption key. In a decryption loop, every time the malware uses a different encryption key derived from a combination of XOR and arithmetic operations. It decrypts the 0x3E184 bytes of the memory buffer to get the final payload.
Figure 4: Single-byte XOR decryption
Next, the shellcode reads the DLL name array, which contains the names of DLLs that are required for the malware to perform its operation. This array is “ole32;oleaut32;wininet;mscoree;shell32”.
The shellcode parses the PEB structure to check for the presence of the above-mentioned DLLs in the loaded modules list and loads the DLL using the “LoadLibraryA” API if they are not present.
Once the required DLLs are loaded into memory, it resolves a few more APIs such as “VirtualProtect”, “SafeArrayCreate”, “CLRCreateInstance” etc., using the API Hashing technique.
AMSI Bypass Using Memory Patching
Next, the shellcode patches the “AmsiScanBuffer” and “AmsiScanString” API, as shown below.
Figure 5: “AmsiScanBuffer” API after patching
Figure 6: “AmsiScanString” API after patching
Disabling Event Tracing (2nd time)
We have observed the second time patching in shellcode to disable Event Tracing, this might be to confirm the patching continues. It patches “EtwEventWrite” API with a single byte “0xCC” (return instruction).
Next, the shellcode starts CLR hosting.
These are the steps required to perform CLR Hosting, in order:
-
Create a CLR MetaHost instance:
ICLRMetaHost* pMetaHost = NULL;
CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost);
-
Enumerate the installed runtimes:
pMetaHost->EnumerateInstalledRuntimes(&installedRuntimes);
Enumerate through runtimes and try to locate a specific dotnet version installed on the system.
One has to use “GetVersionString” method from the ICLRRuntimeInfo interface to find the supported .NET Framework version. This .NET Framework version string will be passed to the GetRuntime API.
-
Get RuntimeInfo using “GetRuntime”:
ICLRRuntimeInfo* runtimeInfo = NULL;
pMetaHost->GetRuntime(sz_runtimeVersion, IID_ICLRRuntimeInfo, (LPVOID*)&runtimeInfo);
-
Get ICorRuntimeHost interface:
ICorRuntimeHost Interface allows more control over the managed runtime from the native code, It can be retrieved using ICLRRuntimeInfo::GetInterface
ICorRuntimeHost* pCorRuntimeHost =NULL;
runtimeInfo->GetInterface(CLSID_CorRuntimeHost,IID_ICorRuntimeHost,(LPVOID*)& pCorRuntimeHost);
-
Retrieve the default AppDomain for the current process:
ICorRuntimeHost interface allows retrieval of the default AppDomain for the current process.
IUnknown* appDomainThunk;
pCorRuntimeHost->GetDefaultDomain(&appDomainThunk);
_AppDomain* defaultAppDomain = NULL;
appDomainThunk->QueryInterface(IID_AppDomain, &defaultAppDomain);
-
Create SafeArray:
we must create SafeArray and copy the MSIL payload to this SafeArray since we can’t provide an unmanaged byte array to the “Load_3” method which loads the assembly into the app domain.
SAFEARRAYBOUND bounds[1];
bounds[0].cElements = sizeof (rawAssemblyByteArray);
bounds[0].lLbound = 0;
SAFEARRAY* safeArray = SafeArrayCreate(VT_UI1, 1, bounds);
SafeArrayLock(safeArray);
memcpy(safeArray->pvData, rawAssemblyByteArray, sizeof (rawAssemblyByteArray));
SafeArrayUnlock(safeArray);
-
Load the assembly to the AppDomain:
_AssemblyPtr managedAssembly = NULL;
defaultAppDomain->Load_3(safeArray, &managedAssembly)
-
Find an entry point to the loaded assembly:
_MethodInfoPtr pMethodInfo = NULL;
managedAssembly->get_EntryPoint(&pMethodInfo)
-
Call the entry point:
pMethodInfo->Invoke_3(VARIANT(), SafeArray_Pointer_To_Arguement , &VARIANT())
The second parameter for the “Invoke_3” function is the SafeArray pointer to the arguments that will be passed to the MSIL payload.
ShellCode Executing Managed Code from a Native Code Using CLR hosting
Next, the shellcode calls the “CLRCreateInstance” API from mscoree.dll. The CLRCreateInstance API returns the new CLR MetaHost instance which will be used by malware to prepare a runtime so it can execute the MSIL AgentTesla payload in memory.
We can see in the below figure that multiple GUIDs have been used while retrieving CLR Hosting Interfaces, for e.g., to retrieve “ICorRuntimeHost” interface, it passed “CLSID_CorRuntimeHost” , “IID_ICorRuntimeHost” as an argument to the “GetInterface” API.
Figure 7: GUID used while CLR hosting
Next, the shellcode retrieves the ICorRuntimeHost interface and starts the CLR.
Figure 8: Call to GetInterface API to retrieve the ICorRuntimeHost interface
Figure 9: Call start method from ICorRuntimeHost interface to start CLR
Next, the shellcode retrieves the default app domain for the current process, as shown below.
Figure 10: Retrieve the default AppDomain for the current process.
Next, the shellcode creates SafeArray using the “SafeArrayCreate“ API by passing an argument as the size of managed code which is 0x3CC00. This SafeArray does have a pointer to the buffer where malware copies the MSIL payload.
Figure 11: Create a SafeArray and copy AgentTesla payload to it
Once a SafeArray was created, it could be loaded into an AppDomain with the “Load_3” method, this “Load_3” method gives a pointer to an Assembly object.
Figure 12: Calls “Load_3” method to load the SafeArray into AppDomain
Next, the shellcode zeros out the MSIL payload from the region where it got decrypted then it destroys the SafeArray using the “SafeArrayDestroy” API.
Finally, the shellcode retrieves the entry point for the assembly and calls the “Invoke_3” method to start the 32-bit MSIL AgentTesla process within the context of the 64-bit native process.
Figure 13: Starts the MSIL AgentTesla process
Figure 14: Browser folder enumerated by 64-bit process once the fileless managed code injection has been done
In Figure 14 above, it looks like the 64-bit process is enumerating the browser folder, but its AgentTesla malware started its execution within the .NET engine.
SonicWall Protections
SonicWall Capture Labs provides protection against analyzed 64-bit executable (4521162d45efc83fa76c4b5c0d405265) as GAV: MalAgent.QZ (Trojan).
This threat was also detected by SonicWall Capture ATP w/RTDMI.
The initial infection vector which is a Word document file has been detected by SonicWall Capture ATP w/RTDMI.
IOCs
Document file:
MD5 : D99020C900069E737B3F4AB8C6947375
SHA256 : A6562D8F34D4C25A94313EBBED1137514EED90B233A94A9125E087781C733B37
64-bit downloaded executable:
MD5 : 4521162D45EFC83FA76C4B5C0D405265
SHA256 : F00ED06A1D402ECF760EC92F3280EF6C09E76036854ABACADCAC9311706ED97D
Shellcode blob:
MD5 : CD485BF146E942EC6BB51351FA42B1FF
SHA256 : 02C03E2E8CA28849969AE9A8AAA7FDE8A8B918B5A29548840367F3ECAC543E2D
Injected AgentTesla Payload:
MD5 : 6999D02AA08B56EFE8B2DBBD6FDC9A78
SHA256 : 7B6867606027BFCA492F95E2197A3571D3332D59B65E1850CB20AA6854486B41
URLs used by malware:
https[:]//New-Coder[.]cc/Users/signed_20240329011751156[.]exe (64-bit exe downloaded)
https[:]//New-Coder[.]cc/Users/shellcodeAny_20240329011339585[.]bin (shellcode downloaded)
Source: Original Post