A Quick Look at BlackWood DLL Loader
Exploring VXUnderground and chanced upon a DLL Loader from 2024, and so why not take a look at it?
Recently, welivesecurity published a post introducing CloudScout
by Evasive Panda - a post-exploitation toolset designed to exfiltrate data from various cloud services using stolen web cookies from different browsers. This revelation piqued my interest in understanding how .NET
binaries (managed code) can be dynamically loaded and executed from unmanaged C/C++ code. Such techniques are not only used in malware operations but also have legitimate applications in software development, such as plugin architectures and hooking mechanisms.
In this blogpost, we’ll delve into the mechanics of hosting the Common Language Runtime (CLR) within unmanaged applications, exploring the necessary COM interfaces and methods. We’ll then look at the malware sample, understand its approach, and provide comprehensive code examples to demonstrate how managed code can be seamlessly integrated into unmanaged environments.
Managed code is executed by the Common Runtime Library (CLR) in the .NET framework. Languages like C#, VB.NET, and F# compile into Intermediate Languages (IL), which the CLR Just-In-Time (JIT) compiles into native code at runtime.
Managed code benefits from features like:
Unmanaged code is executed directly by the operating system. Languages like C and C++ compile into machine code specific to target architecture.
Unmanaged code requires:
Integrating managed code into unmanaged applications allows developers to leverage the rich ecosystem of .NET libraries while maintaining performance-critical components in C/C++. This fusion is beneficial in scenarios like:
Hosting the CLR within an unmanaged application involves interacting with several COM interfaces provided by the .NET framework. Understanding these interfaces is crucial for effectively managing the CLR lifecycle and executing managed code.
The purpose for ICLRMetaHost
is to provide information about the installed CLR versions on a system. It allows the host to enumerate and select the desired CLR version for execution.
Key Method:
GetRuntime
retrieves the ICLRRuntimInfo
interface for a specified CLR version.
ICLRMetaHost* pMetaHost = nullptr;
HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
if (SUCCEEDED(hr) && pMetaHost) {
// Successfully created MetaHost instance
}
The purpose for ICLRRuntimeInfo
is to provide information about a specific CLR version, including its loadability and access to runtime interfaces.
Key Methods:
IsLoadable
checks if CLR can be loaded into the process.GetInterface
retrieves the ICLRRuntimeHost
interface for runtime management. ICLRRuntimeInfo* pRuntimeInfo = nullptr;
hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));
if (SUCCEEDED(hr) && pRuntimeInfo) {
// Successfully retrieved RuntimeInfo
}
The purpose for ICLRRuntimeHost
is to manage the CLR’s lifecycle within the host process. It allows starting and stopping the CLR, accessing application domains, and executing managed methods.
Key Methods:
Start
which initializes and start the CLR.ExecuteInDefaultAppDomain
which executes a specified managed method within the default AppDomain
.ICLRRuntimeHost* pRuntimeHost = nullptr;
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pRuntimeHost));
if (SUCCEEDED(hr) && pRuntimeHost) {
hr = pRuntimeHost->Start();
if (SUCCEEDED(hr)) {
DWORD result = 0;
hr = pRuntimeHost->ExecuteInDefaultAppDomain(
L"path\\to\\MyManagedCode.dll",
L"HAHAHA.ManagedClass",
L"callme",
L"Hello from C++!",
&result
);
// Handle result
}
}
In analyzing the CloudScout
malware, specifically the gmck.dll
component with SHA256 729AEE2C684B05484719199FF2250217B7ACE97671416E6949C496688A777A6F
, we observe that it drops a .NET binary named msvc_4.dll
(also referred to as CGM
) before executing it. This behavior aligns with the technique of dynamically loading and executing managed code from an unmanaged stub.
Malware | SHA256 | Remarks |
---|---|---|
gmck.dll |
729AEE2C684B05484719199FF2250217B7ACE97671416E6949C496688A777A6F | This drops .NET Binary msvc_4.dll AKA CGM before running it |
gmck.dll
writes the msvc_4.dll
to diskCLRCreateInstance
or CorBindToRuntime
CGM.Program.ModuleStart
method within the managed assembly, passing necessary arguments.To gain deeper insights into the malware’s operation, let’s examine the decompiled ModuleStart
function. This function orchestrates the loading and execution of the manage .NET
binary based on the system’s CLR version.
int ModuleStart()
{
// Variable declarations and initializations
Sleep(0x2BF20u); // Initial sleep to evade quick analysis
v31 = 7;
v30 = 0;
LOWORD(Block) = 0;
mw_query_memory_sub_1003E030();
windows_version_identifier = mw_Check_Windows_version_sub_1003DE20(); // Retrieves Windows version
mw_NVIDLA_path_creation_sub_1002B190(Src); // Creates necessary paths
mw_drop_gmck_msvc_4_sub_1002B2D0(Src); // Drops the managed DLL (msvc_4.dll aka CGM)
// Decision to load CLR based on Windows version
if ( windows_version_identifier < 0x60002 )
mw_CorBindToRuntimeEx_sub_1002B650(Src); // Uses CorBindToRuntimeEx for older versions
else
mw_CLRCreateInstance_sub_1002B580(Src); // Uses CLRCreateInstance for newer versions
// Extended sleep post-CLR loading
Sleep(0xEA60u);
if ( m_CreatePathForMalware_sub_1001B6F0() )
{
CreateThread(0, 0, StartAddress, 0, 0, 0); // Spawns a new thread to execute the payload
while (1)
{
// This includes handling different browsers like Chrome, Edge, and Firefox
}
}
if ( v31 >= 8 )
free_1(Block);
return 0;
}
To execute managed .NET
code from an unmanaged C/C++ application, the malware leverages COM interfaces to host the CLR. Depending on the targeted .NET Framework
version, it uses different methods: CLRCreateInstance
for .NET 4.0+
and CorBindToRuntime
for earlier versions.
`
Applicable for: .NET Framework 4.0 and above.
CLRCreateInstance
initializes and retrieves runtime interfaces necessary for hosting the CLR.
The following shows the function prototype:
HRESULT CLRCreateInstance(
REFCLSID clsid,
REFIID riid,
LPVOID *ppInterface
);
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
MetaHost
Instance to access information about the installed CLR version
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));
hr = pRuntimeInfo->IsLoadable(&isLoadable);
ICLRRuntimeHost
interface to start and manage the CLR
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pRuntimeHost));
hr = pRuntimeHost->Start();
hr = pRuntimeHost->ExecuteInDefaultAppDomain(
L"path\\to\\MyManagedCode.dll",
L"HAHAHA.ManagedClass",
L"callme",
L"Hello from C++!",
&result
);
pRuntimeHost->Stop();
pRuntimeHost->Release();
pRuntimeInfo->Release();
pMetaHost->Release();
CoUninitialize();
`
Applicable for: .NET Framework 2.0, 3.0, and 3.5.
CorBindToRuntime
binds the CLR to the host process, allowing the execution of managed code.
Function prototype is as follows:
HRESULT CorBindToRuntimeEx(
LPCWSTR pwzVersion,
LPCWSTR pwzFlavor,
DWORD dwStartupFlags,
REFCLSID rclsid,
REFIID riid,
LPVOID *ppv
);
Sample Functionality Flow:
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
hr = CorBindToRuntimeEx(
L"v2.0.50727",
L"wks",
0,
CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(void**)&pCorRuntimeHost
);
hr = pCorRuntimeHost->Start();
hr = pCorRuntimeHost->ExecuteInDefaultAppDomain(
L"path\\to\\MyManagedCode.dll",
L"HAHAHA.ManagedClass",
L"callme",
L"NOTICE MEEEE",
&result
);
pCorRuntimeHost->Stop();
pCorRuntimeHost->Release();
CoUninitialize();
Note: In the analyzed malware sample, the choice between CLRCreateInstance
and CorBindToRuntime
is based on the detected Windows version, ensuring compatibility with the installed .NET Framework
.
To solidify the understanding on how managed code is executed from unmanaged applications, let’s examine the following code example for both hosting methods.
Before executing managed code from C++, we need a .NET
assembly that exposes a method to be invoked. I have compiled the following code as a library exposing the callme
function.
// HAHAHA/ManagedClass.cs
using System;
namespace HAHAHA
{
public class ManagedClass
{
public static int callme(string argument)
{
Console.WriteLine("Hello from C# method! Argument: " + argument);
return 42;
}
}
}
ICLRCreateInstance
// HostCLR_Method1.cpp
#include <Windows.h>
#include <metahost.h>
#include <comdef.h>
#include <iostream>
#include <string>
#pragma comment(lib, "mscoree.lib") // Link against mscoree.lib
// Helper function to convert HRESULT to readable string
std::wstring GetHRMessage(HRESULT hr) {
_com_error err(hr);
return std::wstring(err.ErrorMessage());
}
bool HostCLRUsingMetaHost() {
HRESULT hr;
ICLRMetaHost* pMetaHost = nullptr;
ICLRRuntimeInfo* pRuntimeInfo = nullptr;
ICLRRuntimeHost* pRuntimeHost = nullptr;
// Initialize COM
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (FAILED(hr)) {
std::wcerr << L"COM Initialization Failed: 0x" << std::hex << hr
<< L" - " << GetHRMessage(hr) << std::endl;
return -1;
}
else {
std::wcout << L"Successfully initialized COM library." << std::endl;
}
// Create CLR MetaHost
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
if (FAILED(hr) || !pMetaHost) {
std::wcerr << L"Failed to create CLR MetaHost instance. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
CoUninitialize();
return -2;
}
else {
std::wcout << L"Created CLR MetaHost instance successfully." << std::endl;
}
// Get the ICLRRuntimeInfo for CLR version 4.0.30319
std::wcout << L"Retrieving CLR runtime information for v4.0.30319..." << std::endl;
hr = pMetaHost->GetRuntime(
L"v4.0.30319", // CLR version to load
IID_PPV_ARGS(&pRuntimeInfo) // Receive ICLRRuntimeInfo
);
if (FAILED(hr) || !pRuntimeInfo) {
std::wcerr << L"Failed to retrieve CLR runtime information. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
pMetaHost->Release();
CoUninitialize();
return -3;
}
else {
std::wcout << L"Retrieved CLR runtime information successfully." << std::endl;
}
// Check if the CLR is loadable
BOOL isLoadable = FALSE;
std::wcout << L"Checking if CLR runtime is loadable..." << std::endl;
hr = pRuntimeInfo->IsLoadable(&isLoadable);
if (FAILED(hr) || !isLoadable) {
std::wcerr << L"CLR runtime is not loadable. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
pRuntimeInfo->Release();
pMetaHost->Release();
CoUninitialize();
return -4;
}
else {
std::wcout << L"CLR runtime is loadable." << std::endl;
}
// Get the ICLRRuntimeHost interface
std::wcout << L"Obtaining ICLRRuntimeHost interface..." << std::endl;
hr = pRuntimeInfo->GetInterface(
CLSID_CLRRuntimeHost, // CLSID of the CLR runtime host
IID_PPV_ARGS(&pRuntimeHost) // Receive ICLRRuntimeHost
);
if (FAILED(hr) || !pRuntimeHost) {
std::wcerr << L"Failed to obtain ICLRRuntimeHost interface. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
pRuntimeInfo->Release();
pMetaHost->Release();
CoUninitialize();
return -5;
}
else {
std::wcout << L"Obtained ICLRRuntimeHost interface successfully." << std::endl;
// Start the CLR
std::wcout << L"Starting the CLR..." << std::endl;
hr = pRuntimeHost->Start();
}
if (FAILED(hr)) {
std::wcerr << L"Failed to start the CLR. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
pRuntimeHost->Release();
pRuntimeInfo->Release();
pMetaHost->Release();
CoUninitialize();
return -6;
}
else {
std::wcout << L"CLR started successfully." << std::endl;
}
// Executing the managed function from the DLL
DWORD result = 0;
std::wcout << L"Executing managed method 'HAHAHA.ManagedClass.callme'..." << std::endl;
hr = pRuntimeHost->ExecuteInDefaultAppDomain(
L"R:\\MAT\\Visual Studio Projects\\learn_dnlib_1\\learn_dnlib_1\\MyManagedCode.dll", // Path to the assembly
L"HAHAHA.ManagedClass", // Fully qualified class name
L"callme", // Method name
L"NOTICE MEEEE", // Argument
&result // Result
);
if (FAILED(hr)) {
std::wcerr << L"Failed to execute managed method in default AppDomain. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
}
else {
std::wcout << L"Successfully executed managed method in default AppDomain." << std::endl;
std::wcout << L"The result from the program is: " << result << std::endl;
}
// Release COM interfaces
if (pRuntimeHost) {
pRuntimeHost->Stop();
pRuntimeHost->Release();
}
if (pRuntimeInfo) pRuntimeInfo->Release();
if (pMetaHost) pMetaHost->Release();
// Uninitialize COM
CoUninitialize();
return 0;
}
CorBindToRuntime
This method should be used for .NET Framework version 2.0, 3.0 or 3.5. To test this out on a Windows 10 machine, I have download Microsoft .NET Framework 3.5 from Official Microsoft Download Center.
bool HostCLRUsingRuntimeHost() {
HRESULT hr;
ICLRRuntimeHost* pCorRunTimeHost;
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (FAILED(hr)) {
std::wcerr << L"COM Initialization Failed: 0x" << std::hex << hr
<< L" - " << GetHRMessage(hr) << std::endl;
return -1;
}
else {
std::wcout << L"Successfully initialized COM library." << std::endl;
}
hr = CorBindToRuntimeEx(
L"v2.0.50727", // CLR Version
L"wks", // Workstation Build
0, // Startup Flag
CLSID_CorRuntimeHost, // CLSID
IID_ICorRuntimeHost, // IID
(PVOID*)&pCorRunTimeHost //Pointer to receive the interface
);
if (FAILED(hr) || !pCorRunTimeHost) {
if (pCorRunTimeHost) {
hr = pCorRunTimeHost->Start();
if (FAILED(hr)) {
std::wcerr << L"Failed to start CLR RuntimeHost. HRESULT: 0x" << std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
}
}
else {
std::wcerr << L"pCorRunTimeHost is null." << std::endl;
}
std::wcerr << L"Failed to create CLR RuntimeHost instance. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
CoUninitialize();
return -2;
}
else {
std::wcout << L"Created CLR RuntimeHost instance successfully." << std::endl;
}
if (FAILED(hr)) {
std::wcerr << L"Failed to start the CLR. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
pCorRunTimeHost->Release();
CoUninitialize();
return -6;
}
else {
std::wcout << L"CLR started successfully." << std::endl;
}
// Executing the managed function from the DLL
DWORD result = 0;
std::wcout << L"Executing managed method 'HAHAHA.ManagedClass.callme'..." << std::endl;
hr = pCorRunTimeHost->ExecuteInDefaultAppDomain(
L"R:\\MAT\\Visual Studio Projects\\learn_dnlib_1\\learn_dnlib_1\\MyManagedCode.dll", // Path to the assembly
L"HAHAHA.ManagedClass", // Fully qualified class name
L"callme", // Method name
L"NOTICE MEEEE", // Argument
&result // Result
);
if (FAILED(hr)) {
std::wcerr << L"Failed to execute managed method in default AppDomain. HRESULT: 0x"
<< std::hex << hr << L" - " << GetHRMessage(hr) << std::endl;
}
else {
std::wcout << L"Successfully executed managed method in default AppDomain." << std::endl;
std::wcout << L"The result from the program is: " << result << std::endl;
}
// Release COM interfaces
if (pCorRunTimeHost) {
pCorRunTimeHost->Stop(); // stop hosting the CLR
pCorRunTimeHost->Release(); //release if data is still present
}
// Uninitialize COM
CoUninitialize();
return true;
}
The full code can be found from the Github
link that was associated with this blogpost.
Previously expanded environment variable : %ProgramData%
from [[#sub_1003E130]],
C:\\ProgramData\\NVIDlA\\gmck
directory if it does not exist.
(N)ovember (V)ictor (I)ndia (D)elta (L)IMA (A)lfa
C:\\ProgramData\\NVIDlA\\gmck\\TEMP
directory if it does not exist.C:\\ProgramData\\NVIDlA\\gmck\\msvc_4.dll
mw_drop_GMCK_sub_1002B3D0
mw_drop_GMCK_sub_1002B3D0
contains the decryption and file dropping logic0x3c1000
bytes of encrypted bytes into buffer8def870f31cf390c0cf2
.NET
malware with the keyC:\\ProgramData\\NVIDlA\\gmck\\msvc_4.dll
The encrypted_buffer.bin
contains encrypted buffer that was extracted from IDA. The following was used to extract and decrypt the encrypted buffer. The decrypted buffer is the content of the CGM
which is a .NET
module.
with open("ENCRYPTED_MSVC_4.bin","wb") as f:
f.write(get_bytes(0x10130490, 0x3C1000))
from malduck import rc4
# Decryption key as bytes
key = b'8def870f31cf390c0cf2'
with open('ENCRYPTED_MSVC_4.bin', 'rb') as file:
encrypted_buffer = unhexlify(file.read())
# Decrypt the buffer using RC4
decrypted_data = rc4(key, encrypted_buffer)
# Save the decrypted data
with open('DECRYPTED_MSVC_4.bin', 'wb') as file:
file.write(decrypted_data)
This is what we have covered previously [[#C++ Hosting Code for Method 1 Using ICLRCreateInstance
]]
BOOL __thiscall mw_CLRCreateInstance_sub_1002B580(void *this)
{
int pRuntimeInfo; // [esp+4h] [ebp-14h] BYREF
int MetaHostObj; // [esp+8h] [ebp-10h] BYREF
int v5; // [esp+Ch] [ebp-Ch] BYREF
int v6; // [esp+10h] [ebp-8h] BYREF
MetaHostObj = 0;
v5 = 0;
pRuntimeInfo = 0;
if ( CLRCreateInstance(&clsid, &REFIID, &MetaHostObj) < 0
|| (*(*MetaHostObj + 12))(MetaHostObj, L"v4.0.30319", &unk_104F65FC, &pRuntimeInfo) < 0
|| (*(*pRuntimeInfo + 36))(pRuntimeInfo, &CLSID_CorRuntimeHost, &IID_ICorRuntimeHost, &v5) < 0
|| (*(*v5 + 12))(v5) < 0 )
{
return 0;
}
v6 = 0;
return (*(*v5 + 44))(v5, this, L"CGM.Program", L"ModuleStart", L" ", &v6) >= 0;
}
We can see the ModuleStart
from CGM.Program
without any parameters. The following shows the screenshot for the actual function that it would invoke from the unmanaged application.
This is also what was previously covered in [[#Method 2 Using `CorBindToRuntime]]
BOOL __thiscall mw_CorBindToRuntimeEx_sub_1002B650(void *dll_path)
{
void *pCorRunTimeHost; // [esp+4h] [ebp-Ch] BYREF
int v4; // [esp+8h] [ebp-8h] BYREF
pCorRunTimeHost = 0;
if ( CorBindToRuntimeEx(L"v2.0.50727", L"wks", 0, &CLSID_CorRuntimeHost, &IID_ICorRuntimeHost, &pCorRunTimeHost) < 0
|| (*(*pCorRunTimeHost + 0xC))(pCorRunTimeHost) < 0 )
{
return 0;
}
v4 = 0;
return (*(*pCorRunTimeHost + 44))(pCorRunTimeHost, dll_path, L"CGM.Program", L"ModuleStart", L" ", &v4) >= 0;
}
This function stores some name of what look like configurations file names.
Gmck
directory at the same directory as the current running malware if it does not exist.<sameDir>\\Gmck.nom.cfg
string<sameDir\\Gmck.stp.cfg
string< 0x60001
C:\\Documents and Settings\\All Users\\Application Data\\Microsoft\\PStatus\\Gmck.nom
C:\\Documents and Settings\\All Users\\Application Data\\Microsoft\\PStatus\\Gmck.stp
>= 0x60001
C:\\Users\\Public\\AppData\\Local\\Windows\\ODBC\\PStatus\\Gmck.nom
C:\\Users\\Public\\AppData\\Local\\Windows\\ODBC\\PStatus\\Gmck.stp
C:\Users\user\AppData\Local\Google\Chrome\User Data\*.*
C:\Users\user\AppData\Local\Google\Chrome\User Data\AutofillStates\Cookies
mw_delete_files_sub_1001B990
for every 300000 ms or 300 s or 5 mins
<sameDir>\\Gmck.nom.cfg
if exists<sameDir>\\Gmck.stp.cfg
if existsC:\\users\\public\\appdata\\local\\windows\\odbc\\PStatus\\Gmck.nom
if existsC:\\users\\public\\appdata\\local\\windows\\odbc\\PStatus\\Gmck.stp
if existsThese are some examples of host-based IOCs:
C:/ProgramData/NVIDlA/gmck/ff_cke<YYYYMMDD_HHMMSS>.dat
C:/ProgramData/NVIDlA/gmck/ff_cke_cfg.dat
C:/ProgramData/NVIDlA/gmck/im_cke_<YYYYMMDD_HHMMSS>.dat
C:/ProgramData/NVIDlA/gmck/im_cke_cfg.dat
C:/ProgramData/NVIDlA/gmck/cm_cke_cfg_<YYYYMMDD_HHMMSS>.dat
C:/ProgramData/NVIDlA/gmck/cm_cke_cfg_<YYYYMMDD_HHMMSS>1.dat
…/TEMP/eg_cke_%s.dat
…/eg_cke_cfg_%s1.dat
%localappdata%/Google/Chrome/User Data/<username>/Cookies
%localappdata%/Google/Chrome/User Data/<username>/Network/Cookies
%localappdata%/Google/Chrome/User Data/Local State
%localappdata%/Microsoft/Edge/User Data/<username>/Cookies
%localappdata%/Microsoft/Edge/User Data/<username>/Network/Cookies
%localappdata%/Microsoft/Edge/User Data/Local State
%AppData%/Mozilla/Firefox/<username>/cookies.sqlite
%AppData%/Mozilla/Firefox/profiles.ini
After cookies are stolen, they are decrypted before being parsed.
Data obtained from here may be decrypted via CryptUnprotectData
.
Signs of Cookie data were also manipulated and stolen here. It is most likely that stolen cookies data from chrome were also stored within configuration files as well after being parsed.
This stealing happens every 3600000ms
which is every hour and stored in configuration files which would be used in the dropped and executed .NET
binary.
It should be noted that this method is not effective against the new App Bound Encryption Feature in starting from Chrome 127 Google Online Security Blog: Improving the security of Chrome cookies on Windows which was designed to make it harder and to prevent threat actor from stealing cookies the way this sample did.
.rdata:104F65EC IID_ICorRuntimeHost db 6Ch ; l ; DATA XREF: mw_CLRCreateInstance_sub_1002B580+73↑o
.rdata:104F65EC ; mw_CorBindToRuntimeEx_sub_1002B650+1C↑o
.rdata:104F65ED db 0A0h
.rdata:104F65EE db 0F1h
.rdata:104F65EF db 90h
.rdata:104F65F0 db 12h
.rdata:104F65F1 db 77h ; w
.rdata:104F65F2 db 62h ; b
.rdata:104F65F3 db 47h ; G
.rdata:104F65F4 db 86h
.rdata:104F65F5 db 0B5h
.rdata:104F65F6 db 7Ah ; z
.rdata:104F65F7 db 5Eh ; ^
.rdata:104F65F8 db 0BAh
.rdata:104F65F9 db 6Bh ; k
.rdata:104F65FA db 0DBh
.rdata:104F65FB db 2
.rdata:104F65FC unk_104F65FC db 0D2h ; DATA XREF: mw_CLRCreateInstance_sub_1002B580+58↑o
.rdata:104F65FD db 0D1h
.rdata:104F65FE db 39h ; 9
.rdata:104F65FF db 0BDh
.rdata:104F6600 db 2Fh ; /
.rdata:104F6601 db 0BAh
.rdata:104F6602 db 6Ah ; j
.rdata:104F6603 db 48h ; H
.rdata:104F6604 db 89h
.rdata:104F6605 db 0B0h
.rdata:104F6606 db 0B4h
.rdata:104F6607 db 0B0h
.rdata:104F6608 db 0CBh
.rdata:104F6609 db 46h ; F
.rdata:104F660A db 68h ; h
.rdata:104F660B db 91h
.rdata:104F660C clsid db 8Dh ; DATA XREF: mw_CLRCreateInstance_sub_1002B580+21↑o
.rdata:104F660D db 18h
.rdata:104F660E db 80h
.rdata:104F660F db 92h
.rdata:104F6610 db 8Eh
.rdata:104F6611 db 0Eh
.rdata:104F6612 db 67h ; g
.rdata:104F6613 db 48h ; H
.rdata:104F6614 db 0B3h
.rdata:104F6615 db 0Ch
.rdata:104F6616 db 7Fh ;
.rdata:104F6617 db 0A8h
.rdata:104F6618 db 38h ; 8
.rdata:104F6619 db 84h
.rdata:104F661A db 0E8h
.rdata:104F661B db 0DEh
.rdata:104F661C REFIID db 9Eh ; DATA XREF: mw_CLRCreateInstance_sub_1002B580+1C↑o
.rdata:104F661D db 0DBh
.rdata:104F661E db 32h ; 2
.rdata:104F661F db 0D3h
.rdata:104F6620 db 0B3h
.rdata:104F6621 db 0B9h
.rdata:104F6622 db 25h ; %
.rdata:104F6623 db 41h ; A
.rdata:104F6624 db 82h
.rdata:104F6625 db 7
.rdata:104F6626 db 0A1h
.rdata:104F6627 db 48h ; H
.rdata:104F6628 db 84h
.rdata:104F6629 db 0F5h
.rdata:104F662A db 32h ; 2
.rdata:104F662B db 16h
.rdata:104F662C CLSID_CorRuntimeHost db 6Eh ; n ; DATA XREF: mw_CLRCreateInstance_sub_1002B580+78↑o
.rdata:104F662C ; mw_CorBindToRuntimeEx_sub_1002B650+21↑o
.rdata:104F662D db 0A0h
.rdata:104F662E db 0F1h
.rdata:104F662F db 90h
.rdata:104F6630 db 12h
.rdata:104F6631 db 77h ; w
.rdata:104F6632 db 62h ; b
.rdata:104F6633 db 47h ; G
.rdata:104F6634 db 86h
.rdata:104F6635 db 0B5h
.rdata:104F6636 db 7Ah ; z
.rdata:104F6637 db 5Eh ; ^
.rdata:104F6638 db 0BAh
.rdata:104F6639 db 6Bh ; k
.rdata:104F663A db 0DBh
.rdata:104F663B db 2
I tried searching for these bytes using the following in VirusTotal.
` content:{ 6C A0 F1 90 12 77 62 47 86 B5 7A 5E BA 6B DB 02 D2 D1 39 BD 2F BA 6A 48 89 B0 B4 B0 CB 46 68 91 8D 18 80 92 8E 0E 67 48 B3 0C 7F A8 38 84 E8 DE 9E DB 32 D3 B3 B9 25 41 82 07 A1 48 84 F5 32 16 6E A0 F1 90 12 77 62 47 86 B5 7A 5E BA 6B DB 02} `
It has gotten me 6 samples in the listing. 5 of 6 samples were detected as malicious while 1 has no positives. It should be noted that not all are associated with CloudScout’s plugin.
Pass.dll (24/74)
Gmck.dll (19/68) (Current Analyzed Sample)
dankdh.dll (16/69)
CGD.Program.ModuleStart
instead which deals with Google Drive Cloud Service after dropping a msvc_4.dll
into NVIDlA/dakdh
folder.ae55fd8280d1f888e66fb6c2d01c5bbb.virus (47/71)
BootstrapDLL.dll (8/72)
Next, I tried to search by RC4 Key as well.
content: {38646566383730663331636633393063}
b2a36442e68848944365d3d1b8b7554a.virus (42/71)
Gmck.dll (19/68) (Current Analyzed Sample)
dankdh.dll (19/69)
88a94ff69e595ca50db210cdfca4d6fc.virus (24/67)
ae55fd8280d1f888e66fb6c2d01c5bbb.virus (47/71)
)Searching for the msvc_4.dll
string is interesting as well.
content: {250073005C006D007300760063005F0034002E0064006C006C000000}
Integration of managed .NET
binaries within unmanaged C/C++ applications presents a powerful mechanism for both legitimate software development and malicious activities. Through the analysis of the sample, we have uncovered how threat actors adeptly utilizes COM interfaces like ICLRMetaHost
, ICLRRuntimeInfo
, and ICLRRuntimeHost
to host the CLR and execute managed code dynamically.
As per the analyzed sample, we see that it was able to select between methods based on system’s CLR version (guessed). This dual-method approach ensures compatibility across wide range of Windows versions and .NET
Framework installations.
Some strategies for development could be to create a .NET Plugin Loaders for enhanced modularity and stealth. Threat actor could design malware that is highly modular. Each plugin, be it for data exfiltration, system reconnaissance, or persistence, can be developed, deployed, and updated independently. This allows for rapid adaptation to new environments and targets without overhauling the entire malware framework.