General
Samplepedia link:
https://samplepedia.cc/sample/5544e6c66cbf6503cddef2797acbff4fb81ededaef2334a596e6484cfaa0b8e8/5/
Hashes
| Type | Hash |
|---|---|
| MD5 | f7821bbcf64f060306d516aeb9453529 |
| SHA1 | 37b65211eaccd54d2db236af59367da500700cb1 |
| SHA256 | 5544e6c66cbf6503cddef2797acbff4fb81ededaef2334a596e6484cfaa0b8e8 |
Summary
- The sample is a
7zarchive - It contains a DLL-sidelading trident consisting of the files:
license.iniDellConnect.exeDellCustomerConnect.dll
- The DLL is a malicious loader:
- It loads the encrypted shellcode payload from
license.ini - It decrypts the payload
- We do not reverse the decryption, but instead dump the decrypted payload by inspecting the arguments passed to
VirtualProtect - The encrypted shellcode payload is not directly called but instead passed as a callback function to the
GrayStringAAPI for stealth
- It loads the encrypted shellcode payload from
- The executed shellcode decrypts an XOR-encrypted payload DLL that is packed / obfuscated by
VMProtect
Analysis
Basic Properties
The file command confirms that it is a 7z file:
$ file DellCustomerConnect.7z
DellCustomerConnect.7z: 7-zip archive data, version 0.4
Using the 7-ZIP list command, we can see the three files that are contained in the archive:
$ 7z l DellCustomerConnect.7z
7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=C.UTF-8 Threads:6 OPEN_MAX:1024
Scanning the drive for archives:
1 file, 7758358 bytes (7577 KiB)
Listing archive: DellCustomerConnect.7z
--
Path = DellCustomerConnect.7z
Type = 7z
Physical Size = 7758358
Headers Size = 243
Method = LZMA2:23 BCJ
Solid = +
Blocks = 2
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2025-10-28 07:22:30 ..... 7714384 7714865 license.ini
2025-10-28 12:19:44 ....A 31920 43250 DellConnect.exe
2025-10-28 02:53:37 ..... 75776 DellCustomerConnect.dll
------------------- ----- ------------ ------------ ------------------------
2025-10-28 12:19:44 7822080 7758115 3 files
Lets note the hashes for completeness:
| File | SHA256 |
|---|---|
license.ini |
1ba04a2544f68212ed0820e732c4fdd798c999a6fb6ada0bb6291006c6e02ba1 |
DellConnect.exe |
be706415e6abcb7df975b085ebae2fa3000dd79d98b436b7941e3dd284ec25d9 |
DellCustomerConnect.dll |
8877244e6bfb497b6766df6d0bb7b6f8e7743aeb967f3b1931fbd096d777ad23 |
Opening DellConnect.exe in radare2, we can see that it imports DellCustomerConnect.dll:
$ r2 DellConnect.exe
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
-- Disable these messages with 'e cfg.fortunes = false' in your ~/.radare2rc
[0x180008000]> aaa
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Running plugin pre-analysis hooks
INFO: Analyze all functions arguments/locals (afva@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Recovering local variables (afva@@@F)
INFO: Type matching analysis for all functions (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Use -AA or aaaa to perform additional experimental analysis
[0x180008000]> ii
nth vaddr bind type lib name
――――――――――――――――――――――――――――――――――――――――――――――――――――――
1 0x180002008 NONE FUNC DellCustomerConnect.dll RHBinder__ShimExeMain
[0x180008000]>
The file license.ini has high entropy, indicating that its content is compressed or encrypted:
$ ent license.ini
Entropy = 7.999975 bits per byte.
This presents the typical hallmarks of DLL Sideloading (T1574.001) where a legitimate signed executable loads a malicious DLL.
Indeed, by looking up the hash of DellConnect.exe on Virustotal we can see that it is not flagged by any antivirus engine and that it has a valid signature.
Let us have a look at the DLL in the following.
DellCustomerConnect.dll
Hashes
| Type | Hash |
|---|---|
| MD5 | 4983e72335d3a4dbee8bd2a3b6bfd06d |
| SHA1 | 09da9e23d7f426c91ed33810be3edb6461a781d8 |
| SHA256 | 8877244e6bfb497b6766df6d0bb7b6f8e7743aeb967f3b1931fbd096d777ad23 |
Basic Properties
The file command shows that it is a 64-bit PE32 DLL:
file DellCustomerConnect.dll
DellCustomerConnect.dll: PE32+ executable (DLL) (GUI) x86-64 (stripped to external PDB), for MS Windows, 12 sections
In radare2 we can also see the compilation timestamp: Wed Jun 25 10:57:22 2025
[0x2176d1320]> i
fd 3
file DellCustomerConnect.dll
size 0x12800
humansz 74K
mode r-x
format pe64
iorw false
block 0x100
type DLL (Dynamic Link Library)
arch x86
baddr 0x2176d0000
binsz 75776
bintype pe
bits 64
canary false
injprot false
retguard false
class PE32+
cmp.csum 0x0001ea63
compiled Wed Jun 25 10:57:22 2025
crypto false
endian little
havecode true
hdr.csum 0x0001ea63
laddr 0x0
lang c++
linenum true
lsyms true
machine AMD 64
nx true
os windows
overlay false
cc ms
pic true
relocs false
signed false
sanitize false
static false
stripped true
uncaps false
subsys Windows GUI
va true
We can also see that the DLL exports two functions (i.e. one of them at least has to contain the malicious functionality):
[0x2176d1320]> iE
nth paddr vaddr bind type size lib name demangled
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1 0x00000860 0x2176d1460 GLOBAL FUNC 0 1937827425144541184.x86_64.tmp RHBinder__Customer
2 0x00000870 0x2176d1470 GLOBAL FUNC 0 1937827425144541184.x86_64.tmp RHBinder__ShimExeMain
By disassembling RHBinder_Customer, we can quickly see that this is a trivial function (that can be discarded from further analysis):
[0x2176d1320]> s 0x2176d1460
[0x2176d1460]> pdf
┌ 6: sym.1937827425144541184.x86_64.tmp_RHBinder__Customer ();
│ 0x2176d1460 b801000000 mov eax, 1
└ 0x2176d1465 c3 ret
The second function RHBinder_ShimExeMain, looks more promising as it calls two other functions before exiting:
[0x2176d1460]> s 0x2176d1470
[0x2176d1470]> pdf
┌ 24: sym.1937827425144541184.x86_64.tmp_RHBinder__ShimExeMain ();
│ 0x2176d1470 4883ec28 sub rsp, 0x28
│ 0x2176d1474 e8c7550000 call fcn.2176d6a40
│ 0x2176d1479 e8025c0000 call fcn.2176d7080
│ 0x2176d147e b801000000 mov eax, 1
│ 0x2176d1483 4883c428 add rsp, 0x28
└ 0x2176d1487 c3 ret
We will analyse this function in BinaryNinja.
RHBinder_ShimExeMain
The function body in HLIL looks similar to what we saw in radare2:

The function sub_2176d6a40 looks like a typical dynamical resolution of API functions using API hashes:

First the kernel32.dll DLL is resolved by name using the function sub_2176d7a40 and its address is stored in rax_3.
Then, the DLL address is passed to the function sub_2176d7da0 together with an API hash to import the API functions.
Given that multiple similar calls with differnt API hashes are performed, several functions from kernel32.dll are loaded.
The returned function addresses are stored in a structure.
For this reason, we rename the function sub_2176d6a40 to mw_fn_dyn_resolve_dependencies.
The function sub_2176d7a40 is renamed to mw_fn_load_dll_by_name and the function sub_2176d7da0 is renamed to mw_fn_resolve_api_hash.
In order to understand the code, we need to figure out how the API hashing algorithm works.
For this reason we take a look at the function mw_fn_resolve_api_hash.
mw_fn_resolve_api_hash
The function starts by resolving the export directory of the passed DLL and looping through its exports, differentiating between 32 and 64 bit binaries.

Then, for each exported function, it computes the hash and compares it against the target value passed as second argument to the function:

This happens in the following steps:
- The constructed API hash is initialized with the value
0x1505 - For each character in the API name the hash is updated by the following algorithm:
mw_construct_hash = mw_construct_hash * 0x21 + mw_curr_chr;
The target hash passed to the function has type int32_t, i.e. it is a 4-byte integer.
The first resolve call reads:
mw_fn_resolve_api_hash(mw_kernel32.dll, data_2176e007c);
and the data stored in data_2176e007c is 0x73cebc5b
The following Python scripts loads a list of kernel32.dll API function names and compares them to a target hash provided on the command-line.
If a match is found, it prints the resolved API hash:
import sys
def resolve(target_hash: int, api: str) -> bool:
construct_hash = 0x1505
for c in api:
construct_hash = (construct_hash * 0x21 + ord(c)) & 0xffffffff
return construct_hash == target_hash
if __name__ == '__main__':
target_hash = int(sys.argv[1], 16)
with open('windows_api_db/kernel32.txt', 'r') as f_kernel32:
kernel32 = f_kernel32.readlines()
for f in kernel32:
if resolve(target_hash, f.strip()):
print("FOUND: ", f.strip())
Running the program gives:
$ python3 resolve_hash.py 0x73cebc5b
FOUND: LocalAlloc
Thus 0x73cebc5b is the 4 byte API hash of LocalAlloc.
The marked up version of the function mw_fn_dyn_resolve_dependencies looks as follows:

The disassembly shows how these three allocated structs storing the resolved API functions are stored:

This concludes the analysis of mw_fn_dyn_resolve_dependencies and we can switch focus to the second function called in RHBinder_ShimExeMain: sub_2176d7080.
From the problem we know that we are dealing with a loader, so we rename this function to mw_fn_loader_main.
mw_fn_loader_main
The function starts by calling a function that performs simple XOR decryption.
We rename the function to mw_fn_decrypt_1:

The function loops over all elements in the buffer provided by the second argument (whose buffer length is the third argument), XORs them based on the XOR-key passed in the fourth argument together with the key length in the fifth argument.
The decrypted buffer is returned in the first argument:

The second and fourth arguments are simply addresses:

They point to the buffers containing the key (green) and encrypted data (red):

We can use Cyberchef to perform the decryption:

The clear text string license.ini matching the file name of the third file contained in the initial 7-ZIP archive increases the confidence that we are on the right track.
In the following, the code extracts the path of the DellCustomerConnect.dll in the function mw_fn_get_module_path and then concatenates it with the decrypted value of license.ini to obtain the path of the file containing the encrypted shellcode:

To extract the parent path of the DLL (which we know is the same as that for license.ini), a call to GetModuleFileNameA is made.
In the result, strrchr looks for the last occurrence of the backslash.
When it is found, the corresponding character is replaced with 0, effectively terminating the string at the parent directory, without including the DLL name.
This is then copied using strcpy and the resulting pointer to the parent directory is returned:

The function mw_fn_read_file_and_process reads the content of the license.ini file and performs some modifications, that at this point are not fully understood.
The following figure shows how CreateFileA is used to obtain a handle to the license.ini file. After GetFileSize is used to obtain its size, a buffer for its content is allocated through VirtualAlloc:

The remainder of the function contains different functions calls that likely modify or decrypt the loaded content of license.ini, although this is not reversed and understood at this point.

The remaining calls of mw_fn_loader_main is shown below:

First the shellcode properties are retrieved through a call to mw_fn_get_shellcode_properties:

VirtualAlloc is used to create a buffer to hold the shellcode, which is then filled with random values by a call to RtlGenRandom.
The return type of the function is a struct of the form:
struct struct_shellcode_properties __packed
{
uint32_t unknown;
uint32_t offset;
uint64_t shellcode_size;
uint64_t shellcode_addr;
};
Again, the remainder of the function is not fully understood.
Then the function mw_fn_copy_bytes_from_2_to_offset_in_1, copies the shellcode that was loaded (and modified / decrypted) from license.ini and copies it to the offset in the shellcode buffer:

The actual execution of the shellcode is performed in the function mw_fn_execute_shellcode_through_GrayString:

VirtualProtect is used to set the memory region properties of the shellcode to 0x40 which corresponds to PAGE_EXECUTE_READWRITE.
Hence this call makes the region executable.
After that, the address of the offset in the loaded shellcode is passed as third argument to the function GrayStringA:
BOOL GrayStringA(
[in] HDC hDC,
[in] HBRUSH hBrush,
[in] GRAYSTRINGPROC lpOutputFunc,
[in] LPARAM lpData,
[in] int nCount,
[in] int X,
[in] int Y,
[in] int nWidth,
[in] int nHeight
);
The type of this argument is GRAYSTRINGPROC and the Microsoft documentation describes it as follows:
A pointer to the application-defined function that will draw the string, or, if TextOut is to be used to draw the string, it is a NULL pointer. For details, see the OutputProc callback function.
Hence, this is a covert way of shellcode execution that avoids direct calls or jumps to the shellcode.
Payload dumping
Without knowing exactly how the shellcode is being decrypted, we now have enough knowledge to dump it in the debugger.
By setting a breakpoint at the VirtualProtect call at address 0x2176d8cce, we can inspect the first and second arguments we can to determine the start address of the shellcode and its size.
Using this, we can dump it.
To get the offset into this shellcode block, we need to break on the call to mw_fn_execute_shellcode_through_GrayString at address 0x2176d8c80 and look up the offset parameter in the struct passed as first argument.
Note that in order to be able to use the addresses in the DLL as shown here, we need to first turn off ASLR.
We start by setting the breakpoints in x64dbg:

As discussed above, the offset is located 4 bytes into the struct. We can see that the value of the offset (little-endian) is: 0x5FD54422 (varies on every run):

Once we have found the address of the buffer passed to virtual protect, we add the offset to arrive at the location of the shellcode:

The address we arrive at looks like shellcode:

The size of the buffer passed as second argument to VirtualProtect is likely too big. Instead, we use the size of the license.ini file (7714384 = 0x75b650):
Then we can dump the decoded shellcode using the following command:
savedata "C:\Users\<REDACTED>\Desktop\shellcode.bin", 000001B5203C4422, 75b650
The extracted payload has the following hash: ff814510658f78c8ce577ea9682f68ff635fb44f697c2fdd8102deff4ecacff0.
It is present on Virustotal (file upload: 2025-10-28 20:23:59), where it is not flagged by any engine.
Shellcode analysis
The shellcode starts by dynamically resolving the depencies through a call to the function resolve_winapi_table_by_hash:

The API resolution code is similar to what we have seen in the initial loader DLL:

This time, the algorithm to compute the API hashes is the following:
def resolve_sc(target_hash: int, api: str) -> bool:
construct_hash = 0x16
for c in api:
construct_hash = (construct_hash * 0x400001 + ord(c)) & 0xffffffff
return construct_hash == target_hash
as can be seen in the following screenshot:

In the following the code collects the addresses of an XOR-key, a payload size and a payload address and XOR-decrypts the payload:

Looking at the corresponding memory region:

we can see that:
- the XOR key reads:
56 06 8a a7 07 02 48 f3 - the size of the (encrypted) payload is:
0x758e00(7704064)
We can use the following command to extract the encrypted payload from the shellcode blob:
dd if=shellcode.bin bs=1 skip=$((0x284d)) count=$((0x758e00)) of=payload.dll.enc
and use Cyberchef to confirm that the XOR-decryption yields a PE file (that will be analysed in the following section):

Payload analysis
The triage view in BinaryNinja shows that the XOR-decrypted payload is a 64-bit DLL with a compilation timestamp from 2025-06-25:

The DLL exports only two functions named DllMain and Starts and has a very limited set of imported functions.
Among the imported functions are LoadLibraryA and GetProcAddress which hint a dynamic resolution of dependencies at runtime:

The section information shown in radare2 indicates several mismatches between raw and virtual section sizes, hinting at some sort of packing mechanism:
[0x34d307bb1]> iS entropy
nth paddr size vaddr vsize perm flags entropy type name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000000 0x0 0x34cef1000 0x18000 -r-x 0x60000020 ---- .text
1 0x00000000 0x0 0x34cf09000 0x1000 -rw- 0xc0000040 ---- .data
2 0x00000000 0x0 0x34cf0a000 0x1000 -r-- 0x40000040 ---- .rdata
3 0x00000000 0x0 0x34cf0b000 0x1000 -r-- 0x40000040 ---- .pdata
4 0x00000000 0x0 0x34cf0c000 0x1000 -r-- 0x40000040 ---- .xdata
5 0x00000000 0x0 0x34cf0d000 0x1000 -rw- 0xc0000080 ---- .bss
6 0x00000000 0x0 0x34cf0e000 0x1000 -r-- 0x40000040 ---- .edata
7 0x00000000 0x0 0x34cf0f000 0x1000 -rw- 0xc0000040 ---- .idata
8 0x00000000 0x0 0x34cf10000 0x1000 -rw- 0xc0000040 ---- .CRT
9 0x00000000 0x0 0x34cf11000 0x1000 -rw- 0xc0000040 ---- .tls
10 0x00000000 0x0 0x34cf12000 0x3f3000 -r-x 0x60000020 ---- .l7M
11 0x00000400 0x600 0x34d305000 0x1000 -rw- 0xc0000040 0.18254678 ---- .Dz'
12 0x00000a00 0x758200 0x34d306000 0x759000 -r-x 0x68000060 7.73831715 ---- .m#E
13 0x00758c00 0x200 0x34da5f000 0x1000 -r-- 0x42000040 2.22132746 ---- .reloc
Malcat detects VMProtect (which is to be expected from the task description):
