This analysis covers a multi-stage attack chain designed for stealth and persistence: starting with a PDF-themed phishing lure that uses DLL sideloading via a legitimate Logitech binary, it employs Alcatraz obfuscation (CFF and instruction mutation) and a VEH-based execution flow to hide its activity. The malware maintains persistence through COM Hijacking, eventually reflectively loading a Cobalt Strike beacon that connects to a remote C2 server.
The folder containing the malware is as follows:

Among them, 2 dll files and a pdf are set to hidden:
1. V-HDLD-0009-465866.pdf
It is a normal pdf file, without js.
2. V-HDLD-0009-465866.pdf…exe
This is a legitimate executable file from Logitech.
SHA256: a3f9b61cbd4aab9763c43e2c90e4894fc0df489a7bae3dac7830a5c534324531
The file imports the RunDJCU function from DJCU.dll.

3. DJCU.dll
Viewing the RunDJCU function with IDA:

The code is heavily obfuscated, making it impossible for IDA to recognize the function, and it cannot be made into a function yet.
Not only RunDJCU, many other functions are also similarly obfuscated.
3.1. Deobfuscation
The malware uses a binary obfuscator tool named Alcatraz. The obfuscation techniques used include:
a. Jmp $+1

To deobfuscate, simply nop the EB byte in the jmp $+1 instruction and re-analyze the code.
After deobfuscation, we get the following code:

b. Obfuscate mov REG, instruction
The malware obfuscates the instruction:
mov REG, <imm>
into the following equivalent instruction set:
mov REG, <imm>
pushf
push r10
push r12
push r14
not REG
rol r10, <imm>
add REG, <imm>
xchg r14, r12
pop r14
pop r12
pop r10
xor REG, <imm>
rol REG, <imm>
popf
Note: <imm> just refers to an immediate value, the <imm> values are not necessarily equal.
Example:

Equivalent to mov eax, 0.
To deobfuscate this technique, we need to search for the pattern of this instruction block, then calculate the value of the target REG, patch it with mov REG, <imm>, and nop the rest.

After deobfuscation:

c. Obfuscate lea REG64, [rip+offset] instruction

This instruction is equivalent to setting r8 = cs:1C59840D1h-45917A31h.
We need to patch this instruction with lea r8, [rip+offset] and nop the rest.
We cannot calculate and hardcode the value of r8 because the address used here is relative. If an absolute address is used, it will not ensure that r8 points correctly when the dll is loaded at different base addresses.
After deobfuscation, the above instruction set becomes:

We can see that r8 references the string ‘Global\FC3D587B-54A1-48EB-9A5E-53923C663C95’.
Obviously, we cannot deobfuscate and patch everything manually. A feasible approach is to use IDA python, writing a script to automatically find and patch the code.
d. Control flow flattening
After deobfuscating the above techniques, IDA is able to generate pseudocode for RunDJCU.


However, the generated code is quite ugly.
Here we can see another obfuscation technique, which is Control Flow Flattening.
Instead of letting the code execute and branch naturally, the program obfuscates it by creating a dispatcher, used to direct the execution flow based on the value of eax.
Below is the dispatcher used in 1 function:

For example, with eax = 0, the flow jumps to loc_18007A04A.
After executing the main code, at the end of loc_18007A04A, eax will be set to a different value, then the flow jumps back to the dispatcher and continues to be routed to the next loc_.
push rax; pushf; mov eax,<next_state>; NOP sled; jmp dispatcher

To deobfuscate, instead of jumping back to the dispatcher after executing a loc_, we can calculate the next loc_ based on the set eax and jump directly to it, without needing to go through the dispatcher anymore.
Once the jumps are corrected, the next step is to nop out the entire dispatcher.
After unflattening as described above, we obtain the RunDJCU function with very clean code.

3.2. Behavior Analysis
After renaming the functions based on their functionality:

In the EstablishPersistenceAndCopyPayloads function:
The malware copies DJCU.dll, WorkFolderShell.dll, and V-HDLD-0009-465866.pdf…exe into %APPDATA%\Microsoft\Windows.
Specifically, V-HDLD-0009-465866.pdf…exe is copied with the name skype.exe.



It creates the key HKCU\SOFTWARE\Classes\CLSID\{97D47D56-3777-49FB-8E8F-90D7E30E1A1E}\InprocServer32
And sets its value to the path of the WorkFolderShell.dll file.


This is a persistence technique mentioned in technique T1546.015. When Windows loads the “Work Folder Logon Trigger” (which usually happens at logon), the dll will be called. Next, to avoid suspicion from the user, the malware displays the pdf file normally (via chrome or msedge).

DJCU.dll also applies another quite interesting technique to hide the execution flow: it registers a VEH handler as follows:


We can see that the malware calls the AntiDebug_BreakpointObfuscation function multiple times.

Each time this function is called, when __debugbreak() is executed, the program generates an EXCEPTION_BREAKPOINT exception, and the Handler will be triggered.
When __debugbreak() is hit 70 times, the program allocates a memory region of size 308813 bytes.
When hit 200 times, it copies g_EncryptedData into the allocated memory region.

g_EncryptedData is the data at the .WJiYW5r section.
When hit 210 times, it decrypts the data by XORing it with the string WJiYW5r.

When hit 220 times, it calls the shellcode at offset 4683.
3.3. Shellcode Analysis
Debugging into the shellcode:

Function sub_207E9A59061:

Looking through the sub-functions, sub_207E9A59061 looks quite like a PE Reflective loader.
Function sub_207E9A593E1:

This function is searching for the beginning of the PE based on signatures.
It returns the address immediately after the initial 2 nop instructions of the entrypoint of the shellcode.

Dumping 308813 - 4683 - 2 = 304128 bytes from this address, we get a PE file.
3.4. Unpack Beacon
Opening with IDA: The export function only has 2 functions.
![]()
The code at DllEntryPoint cannot be recognized.
![]()
The IAT table also contains strange characters entirely.
![]()
We can see that the DllEntryPoint and IAT have been encrypted.
The ReflectiveLoader function (seen previously in the shellcode region) is indeed for self-loading and decrypting data.
![]()
There are quite a few variables colored red, meaning ‘VALUE MAY BE UNDEFINED’, and many functions have their parameters incorrectly recognized, whereas when looking at the pseudocode for this function in the shellcode, the code looks quite clean.
The reason is because the way this function is called is quite unusual. Normally, a function starts with push rbp; mov rbp, rsp; sub rsp, <stack frame>.
But this function doesn’t have push rbp; mov rbp, rsp, meaning the stack is not properly aligned, so if this function stands alone, IDA cannot know the correct stack.
![]()
Meanwhile, when viewed with the shellcode, before calling the function, there is a piece of code that helps align the new stack frame.
![]()
So when jumping into the ReflectiveLoader function, IDA already has rbp, rsp to reference to create the correct stack.
After self-loading and decrypting, ReflectiveLoader calls DllEntryPoint.
We could dump and rebuild the dll after it finishes decrypting, but there are a few issues to be aware of:
a. PE Header
CopyPEHeader
![]()
When debugging to CopyPEHeader, we need to ensure that if ( numberOfSections ) does not branch down to return, but runs to qmemcpy so that the header is copied successfully.
Furthermore, do not branch into ‘Clear signature’, because then the PE signature like MZ will be removed, making tools unable to recognize the PE for dumping.
We can control the flow by editing the value to write JZ.
b. Fix IAT
ResolveImports
![]()
The function does not decrypt the IAT within the PE image, but only copies the dll name and function name into another buffer, decrypts it, resolves the function address, and then fills that address into the IAT.
This means the dll names and function names in the IAT remain in an encrypted state.
We need to set a breakpoint and write a hook script at the qmemcpy code snippet to extract the address containing the encrypted dll name, function name, and data, then decrypt it and patch it back into the IAT.
Hook script:
import json
import os
import idc
import ida_bytes
from ida_dbg import refresh_debugger_memory
LOG_FILE = r"C:\Temp\breakpoint_dump_qmemcpy.json"
REG_DEST = "RDI"
REG_SRC = "RSI"
def log_hit():
try:
refresh_debugger_memory()
dest_addr = idc.get_reg_value(REG_DEST)
src_addr = idc.get_reg_value(REG_SRC)
data_raw = idc.get_bytes(src_addr, 0x40)
if data_raw:
data_hex = data_raw.hex()
else:
data_hex = "<read_error>"
entry = {
"dest": hex(dest_addr),
"src": hex(src_addr),
"data": data_hex
}
existing_data = []
if os.path.exists(LOG_FILE):
try:
with open(LOG_FILE, "r") as f:
content = f.read()
if content.strip():
existing_data = json.loads(content)
except Exception as e:
print(f"Error reading existing JSON: {e}")
existing_data.append(entry)
with open(LOG_FILE, "w") as f:
json.dump(existing_data, f, indent=2)
print(f"Logged qmemcpy hit: dest={entry['dest']}, src={entry['src']}")
except Exception as e:
print(f"Breakpoint Script Error: {e}")
log_hit()
The collected data is as follows:
[
{
"dest": "0x24e4c5a6fc0",
"src": "0x24e4c5905ca",
"data": "c7b48610c9bde76ca295b8328c007401c59ca43bfe82bb30ed85b110ed9cb13adc98a43bcf9dbd3be285d400f701c381b130dc83bb3de982a70ae39ab1308c00"
},
{
"dest": "0x24e4c5a6fc0",
"src": "0x24e4c590124",
"data": "cf83b13ff8949a3fe194b00ee581b11f8c00ce04d894a633e59fb52ae9a1a631ef94a72d8c00a400cf83b13ff894842ce392b12dffb0d400c501cb94a01df983"
}
]
The data after decoding looks like this:

It can be seen that the dll name and function name have been successfully decrypted.
After the ResolveImports function finishes executing, we patch the IAT:
import ida_bytes
import ida_kernwin
import struct
import json
KEY = 1591013772
def decrypt_string(data: bytes, key: int) -> bytes:
'''XOR decrypt with 4-byte key (little-endian)'''
if key == 0:
return data
k = struct.pack('<I', key)
out = bytearray(data)
for i in range(len(out)):
out[i] ^= k[i % 4]
return bytes(out)
with open("breakpoint_dump_qmemcpy.json", "r") as f:
qmemcpy_logs = json.load(f)
patched_count = 0
for log in qmemcpy_logs:
data_hex = log['data']
src_addr = int(log['src'], 16) if isinstance(log['src'], str) else log['src']
decrypted = decrypt_string(bytes.fromhex(data_hex), KEY)
null_pos = decrypted.find(b'\x00')
if null_pos != -1:
decrypted_str = decrypted[:null_pos + 1] # Include the \x00
else:
decrypted_str = decrypted + b'\x00' # Add null if missing
ida_bytes.patch_bytes(src_addr, decrypted_str)
printable = decrypted_str[:-1].decode(errors='ignore')
print(f"0x{src_addr:X}: {printable}")
patched_count += 1
print(f"\nPatched {patched_count} strings")
ida_kernwin.info(f"Successfully patched {patched_count} decrypted strings")
c. Dump process
Continue running to load the PE, running up to the code that calls DllEntryPoint.
![]()
At this point, we can dump the process to extract the dll.
![]()
We obtain:
![]()
24e4c550000.dll is the dll we are looking for.
Opening it with IDA, we see that the IAT has been fully decrypted.
![]()
DllEntryPoint has also been successfully decrypted.
![]()
3.5. Analyze Beacon
![]()
InitAgentConfig
![]()
C2PollLoop
![]()
![]()
The configuration of this beacon is stored in a global variable.
![]()
We use the tool SentinelOne/CobaltStrikeParser to parse the beacon config:
{
"BeaconType": [
"Hybrid HTTP DNS"
],
"Port": 1,
"SleepTime": 10000,
"MaxGetSize": 1399652,
"Jitter": 47,
"MaxDNS": 255,
"PublicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFE5CHlJBBJWLQICYJBg63wu4gw6MmhaoxV66GkLwhNYITzr91LW+EL95JCuVJh+54+ach6/PgFX19LUftoWu/rmiWAxC+mbXAvPeujaGUrRuERroVFulRFyPHiGj2yIJWBU/ssarYHGHM+XFknatv+BqVq9elpvMGjuASYbffywIDAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"PublicKey_MD5": "4036667c1a9d8ffde93df2273aa51368",
"C2Server": "ns2.welslanguageschool.com,/preload",
"UserAgent": "Not Found",
"HttpPostUri": "Not Found",
"Malleable_C2_Instructions": "Not Found",
"HttpGet_Metadata": "Not Found",
"HttpPost_Metadata": "Not Found",
"SpawnTo": "AAAAAAAAAAAAAAAAAAAAAA==",
"PipeName": "Not Found",
"DNS_Idle": "8.8.8.8",
"DNS_Sleep": 0,
"SSH_Host": "Not Found",
"SSH_Port": "Not Found",
"SSH_Username": "Not Found",
"SSH_Password_Plaintext": "Not Found",
"SSH_Password_Pubkey": "Not Found",
"SSH_Banner": "",
"HttpGet_Verb": "GET",
"HttpPost_Verb": "POST",
"HttpPostChunk": 96,
"Spawnto_x86": "%windir%\\syswow64\\rundll32.exe",
"Spawnto_x64": "%windir%\\sysnative\\rundll32.exe",
"CryptoScheme": 0,
"Proxy_Config": "Not Found",
"Proxy_User": "Not Found",
"Proxy_Password": "Not Found",
"Proxy_Behavior": "Use IE settings",
"Watermark_Hash": "NtZOV6JzDr9QkEnX6bobPg==",
"Watermark": 987654321,
"bStageCleanup": "True",
"bCFGCaution": "False",
"KillDate": 0,
"bProcInject_StartRWX": "True",
"bProcInject_UseRWX": "True",
"bProcInject_MinAllocSize": 0,
"ProcInject_PrependAppend_x86": "Empty",
"ProcInject_PrependAppend_x64": "Empty",
"ProcInject_Execute": [
"CreateThread",
"SetThreadContext",
"CreateRemoteThread",
"RtlCreateUserThread"
],
"ProcInject_AllocationMethod": "VirtualAllocEx",
"ProcInject_Stub": "rlr8/ugCZnTcjztPLaRsfw==",
"bUsesCookies": "False",
"HostHeader": "",
"smbFrameHeader": "AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"tcpFrameHeader": "AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"headersToRemove": "Not Found",
"DNS_Beaconing": "ntp.",
"DNS_get_TypeA": "ntp-a.",
"DNS_get_TypeAAAA": "ntp-4a.",
"DNS_get_TypeTXT": "ntp-tx.",
"DNS_put_metadata": "ntp-mx",
"DNS_put_output": "ntp-ox.",
"DNS_resolver": "",
"DNS_strategy": "failover",
"DNS_strategy_rotate_seconds": -1,
"DNS_strategy_fail_x": 5,
"DNS_strategy_fail_seconds": -1,
"Retry_Max_Attempts": 0,
"Retry_Increase_Attempts": 0,
"Retry_Duration": 0
}
4. WorkFolderShell.dll
This dll exports a few functions.

Except for DllEntryPoint, when calling other functions, this dll only forwards the call to legitimate dlls, and it itself does not contain the code for these functions.

DllEntryPoint calls a function to run the process %APPDATA%\Microsoft\Windows\skype.exe in the background (which was renamed from the initial V-HDLD-0009-465866.pdf…exe).

IOCs
1. Mutex:
Global\FC3D587B-54A1-48EB-9A5E-53923C663C95
2. Registry:
Computer\HKEY_CURRENT_USER\SOFTWARE\Classes\CLSID\{97D47D56-3777-49FB-8E8F-90D7E30E1A1E}\InprocServer32
Value: %APPDATA%\Microsoft\Windows\WorkFolderShell.dll
3. Files:
%APPDATA%\Microsoft\Windows\skype.exe
SHA256: 43583CE6CE1A3FC6FA92A89DA7307BBF86DB429ED3C1FAC11BB534F2EC146DA3
%APPDATA%\Microsoft\Windows\DJCU.dll
SHA256: 848C0F01467A1C69895557D80BD15DDD242331F8FF27693AE413E12878DDF12A
%APPDATA%\Microsoft\Windows\WorkFolderShell.dll
SHA256: 9F1C7208FBA35C8E4608F634BD1A1E8571213AF73E9C9CDDDA2F20A59BF5DFD1
4. Beacon config:
BeaconType - Hybrid HTTP DNS
Port - 1
SleepTime - 10000
MaxGetSize - 1399652
Jitter - 47
MaxDNS - 255
PublicKey_MD5 - 4036667c1a9d8ffde93df2273aa51368
C2Server - ns2.welslanguageschool.com,/preload
UserAgent - Not Found
HttpPostUri - Not Found
Malleable_C2_Instructions - Not Found
HttpGet_Metadata - Not Found
HttpPost_Metadata - Not Found
PipeName - Not Found
DNS_Idle - 8.8.8.8
DNS_Sleep - 0
SSH_Host - Not Found
SSH_Port - Not Found
SSH_Username - Not Found
SSH_Password_Plaintext - Not Found
SSH_Password_Pubkey - Not Found
SSH_Banner -
HttpGet_Verb - GET
HttpPost_Verb - POST
HttpPostChunk - 96
Spawnto_x86 - %windir%\syswow64\rundll32.exe
Spawnto_x64 - %windir%\sysnative\rundll32.exe
CryptoScheme - 0
Proxy_Config - Not Found
Proxy_User - Not Found
Proxy_Password - Not Found
Proxy_Behavior - Use IE settings
Watermark_Hash - NtZOV6JzDr9QkEnX6bobPg==
Watermark - 987654321
bStageCleanup - True
bCFGCaution - False
KillDate - 0
bProcInject_StartRWX - True
bProcInject_UseRWX - True
bProcInject_MinAllocSize - 0
ProcInject_PrependAppend_x86 - Empty
ProcInject_PrependAppend_x64 - Empty
ProcInject_Execute - CreateThread
SetThreadContext
CreateRemoteThread
RtlCreateUserThread
ProcInject_AllocationMethod - VirtualAllocEx
bUsesCookies - False
HostHeader -
headersToRemove - Not Found
DNS_Beaconing - ntp.
DNS_get_TypeA - ntp-a.
DNS_get_TypeAAAA - ntp-4a.
DNS_get_TypeTXT - ntp-tx.
DNS_put_metadata - ntp-mx
DNS_put_output - ntp-ox.
DNS_resolver -
DNS_strategy - failover
DNS_strategy_rotate_seconds - -1
DNS_strategy_fail_x - 5
DNS_strategy_fail_seconds - -1
Retry_Max_Attempts - 0
Retry_Increase_Attempts - 0