- Published on
i cracked a $200 software protection in a day with xcopy
- Authors
- Name
- vmfunc
- @vmfunc
- computer wizard
disclaimer: this is educational security research only. i do not condone piracy. i purchased a legitimate license for this software and conducted this analysis on my own property. this writeup exists to document protection implementation flaws, not to enable theft. support developers - buy their software.
github repo: vmfunc/enigma
tl;dr
i spent a day analyzing enigma protector - a $200 commercial software protection system used by thousands of vendors. RSA cryptographic signatures, hardware-bound licensing, anti-debugging, VM-based code obfuscation. serious enterprise security theater.
then i noticed the protected installer extracts a completely unprotected payload to disk.
xcopy /E "C:\Program Files\...\product" .\crack\
that’s the entire crack. copy the installed files. they run on any machine. no keygen needed, no binary patching, no cryptanalysis.
$200 protection defeated by a command that shipped with DOS 3.2 in 1986.
this is a case study in why threat modeling matters more than fancy cryptography, and why “military-grade encryption” means nothing when you leave the back door wide open.
target overview
bass bully premium - a VST3 synthesizer plugin. protected by enigma protector, a commercial software protection system that costs $250+ and promises serious security.
from their marketing:
“Enigma Protector is a powerful tool designed to protect executable files from illegal copying, hacking, modification and analysis.”
we’ll see about that.
we have one known valid license:
Key: GLUJ-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-V99KP3
HWID: 3148CC-XXXXXX
Name: Bass Bully
our goal: understand the protection and build a proper crack
static analysis
the PE headers
first, let’s look at what we’re dealing with:
import pefile
pe = pefile.PE(r"Bass Bully Premium_Installer_win64.exe")
print(f"Machine: {'x64' if pe.FILE_HEADER.Machine == 0x8664 else 'x86'}")
print(f"Sections: {pe.FILE_HEADER.NumberOfSections}")
print(f"Entry Point: 0x{pe.OPTIONAL_HEADER.AddressOfEntryPoint:X}")
print(f"Image Base: 0x{pe.OPTIONAL_HEADER.ImageBase:X}")
Machine: x64
Sections: 9
Entry Point: 0x16485D0
Image Base: 0x140000000
that entry point is suspicious. 0x16485D0 is way into the binary.. typical of packed executables where the real entry point is hidden. normal programs start around 0x1000.
string hunting
with open(pe_path, 'rb') as f:
data = f.read()
for target in [b'Enigma Protector', b'enigmaprotector']:
offset = 0
while (idx := data.find(target, offset)) != -1:
print(f"0x{idx:08X}: {target.decode()}")
offset = idx + 1
0x0040972B: Enigma Protector
0x00409746: Enigma Protector
0x00409786: Enigma Protector
0x00409BA8: Enigma Protector
0x00409BC3: Enigma Protector
0x0040A038: Enigma Protector
0x0040A053: Enigma Protector
0x004099BF: enigmaprotector
0x00409DDA: enigmaprotector
confirmed: enigma protector. now we know what we’re dealing with.
what about the network?
does this even phone home..?
imports = [entry.dll.decode() for entry in pe.DIRECTORY_ENTRY_IMPORT]
kernel32.dll, user32.dll, advapi32.dll, oleaut32.dll, gdi32.dll,
shell32.dll, version.dll, ole32.dll, COMDLG32.dll, MSVCP140.dll, ...
no winhttp.dll, wininet.dll, or ws2_32.dll. offline validation only. all crypto is local, so theoretically extractable.
this is good news!! online validation would require MITM or server emulation. offline means everything we need is in the binary.
the enigma protector internals
enigma protector is a commercial protection system that provides:
- code virtualization - transforms x86/x64 into proprietary VM bytecode
- anti-debugging -
IsDebuggerPresent, timing checks, hardware BP detection - anti-tampering - CRC checks on packed sections
- registration API - HWID-bound licensing with RSA signatures
you can read about all these features on their documentation page. they’re pretty thorough about explaining what they protect against. they just didn’t think someone would… not use it on the payload.
the registration API
according to enigma’s SDK, these functions are exposed:
int EP_RegCheckKey(const char* name, const char* key);
const char* EP_RegHardwareID(void);
void EP_RegSaveKey(const char* name, const char* key);
void EP_RegLoadKey(char* name, char* key);
these aren’t normal exports; they’re resolved dynamically after enigma unpacks itself. you can’t just GetProcAddress them from outside. you have to either:
- hook them at runtime after unpacking
- pattern scan the unpacked memory
- be an absolute clown and just not use them in your payload (definitely not foreshadowing)
the entry point
using capstone to disassemble the entry point:
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
entry_rva = pe.OPTIONAL_HEADER.AddressOfEntryPoint
entry_offset = pe.get_offset_from_rva(entry_rva)
with open(pe_path, 'rb') as f:
f.seek(entry_offset)
code = f.read(64)
md = Cs(CS_ARCH_X86, CS_MODE_64)
base = pe.OPTIONAL_HEADER.ImageBase + entry_rva
for insn in md.disasm(code, base):
print(f"0x{insn.address:X}: {insn.mnemonic:8} {insn.op_str}")
0x1416485D0: jmp 0x1416485da ; skip garbage bytes
0x1416485D2: add byte ptr [rsi + 0x40], dl
0x1416485D8: add byte ptr [rax], al
0x1416485DA: push rax ; real code starts here
0x1416485DB: push rcx
0x1416485DC: push rdx
0x1416485DD: push rbx
0x1416485DE: push rbp
0x1416485DF: push rsi
0x1416485E0: push rdi
0x1416485E1: push r8
0x1416485E3: push r9
the jmp-over-garbage pattern is classic anti-disassembly. linear disassemblers will try to decode the garbage bytes between jmp and its target, producing nonsense. the real unpacker starts at 0x1416485DA with a standard register preservation sequence before calling the enigma loader.
phase 3: key format analysis
we have a known valid key. let’s understand its structure before we try to break it.
GLUJ-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-V99KP3
structure breakdown
- 8 groups separated by dashes
- groups 0-6: 4 characters each
- group 7: 6 characters (larger - likely checksum/signature)
- character set: 0-9, A-Z (base36)
base36 decoding
key = "GLUJ-QE58-U3Z4-RQTJ-K7GJ-JXZ5-CVK5-V99KP3"
groups = key.split('-')
for i, group in enumerate(groups):
val = int(group, 36)
bits = val.bit_length()
print(f"[{i}] {group:6} = {val:10} (0x{val:08X}) {bits:2} bits")
[0] GLUJ = 774811 (0x000BD29B) 20 bits
[1] QE58 = 1231388 (0x0012CA1C) 21 bits
[2] U3Z4 = 1404832 (0x00156FA0) 21 bits
[3] RQTJ = 1294471 (0x0013C087) 21 bits
[4] K7GJ = 942787 (0x000E62C3) 20 bits
[5] JXZ5 = 930497 (0x000E32C1) 20 bits
[6] CVK5 = 600773 (0x00092AC5) 20 bits
[7] V99KP3 = 1890014727 (0x70A75607) 31 bits <- significantly larger
interesting. group 7 is way bigger than the others. that’s probably a truncated cryptographic signature.
cryptographic structure
the key structure appears to be:
[ DATA: ~143 bits ] [ SIGNATURE: 31 bits ]
Groups 0-6 (7 x ~20 bits) Group 7 (truncated)
enigma uses RSA for signing. the full signature would be much larger, but they truncate it to fit the key format. this means:
- public key is embedded in the protected binary
- key validation = RSA signature verification
- keygen would require extracting and factoring the RSA modulus
RSA with small key sizes is technically factorable with enough compute. but that’s a lot of work for… well, you’ll see.
HWID format
hwid = "3148CC-059521"
parts = hwid.split('-')
# Two 24-bit values = 48 bits total hardware fingerprint
HWID is derived from hardware characteristics (CPU ID, disk serial, MAC address, etc). the key is cryptographically bound to this value, so a key generated for one machine won’t work on another.
this is actually decent protection! if they used it properly! (they didn’t lmao)
the pivot
at this point i’m preparing to either factor the RSA key or do runtime hooking to bypass validation. then i thought: wait, what are we actually protecting here?
let me just check the installed VST real quick…
analyzing the installed VST
vst_path = r"C:\Program Files\Common Files\VST3\Bass Bully VST\Bass Bully Premium.vst3"
vst_dll = vst_path + r"\Contents\x86_64-win\Bass Bully Premium.vst3"
pe_vst = pefile.PE(vst_dll)
print(f"Size: {os.path.getsize(vst_dll):,} bytes")
print("Imports:")
for entry in pe_vst.DIRECTORY_ENTRY_IMPORT:
print(f" {entry.dll.decode()}")
Size: 7,092,736 bytes
Imports:
KERNEL32.dll
USER32.dll
GDI32.dll
SHELL32.dll
ole32.dll
OLEAUT32.dll
MSVCP140.dll
WINMM.dll
IMM32.dll
dxgi.dll
VCRUNTIME140.dll
VCRUNTIME140_1.dll
api-ms-win-crt-runtime-l1-1-0.dll
...
wait. where are the enigma imports?
hunting for protection
with open(vst_dll, 'rb') as f:
data = f.read()
for term in [b'Enigma', b'EP_Reg', b'Registration', b'HWID', b'enigma']:
count = data.count(term)
print(f"{term.decode():15} : {count} occurrences")
Enigma : 0 occurrences
EP_Reg : 0 occurrences
Registration : 0 occurrences
HWID : 0 occurrences
enigma : 0 occurrences
zero.
hold on.
$ strings "Bass Bully Premium.vst3" | grep -i enigma
$ strings "Bass Bully Premium.vst3" | grep -i regist
nothing. no output… ?????????
you have got to be kidding me.
the VST has absolutely no protection. it’s a clean JUCE framework build. no enigma runtime. no license callbacks. no protection whatsoever.
they protected the installer. not the payload. THE INSTALLER. NOT THE ACTUAL PRODUCT.
i can’t even be mad. this is genuinely hilarious.
the vulnerability
here’s what’s happening:
+-------------------------------------------------------------------+
| ENIGMA PROTECTOR |
| +--------------------------------------------------------------+ |
| | Installer.exe | |
| | [x] RSA key verification | |
| | [x] HWID binding | |
| | [x] Anti-debug, anti-tamper | |
| | [x] Code virtualization | |
| | | | |
| | v | |
| | +--------------------------------------------------------+ | |
| | | Payload (extracted on install) | | |
| | | - Bass Bully Premium.vst3 <- ZERO PROTECTION lol | | |
| | | - Bass Bully Premium.rom <- NOT EVEN ENCRYPTED | | |
| | +--------------------------------------------------------+ | |
| +--------------------------------------------------------------+ |
+-------------------------------------------------------------------+
the entire protection stack only controls whether the installer runs. once files hit disk, the protection is basically useless.
this is like putting a vault door on a tent.
what they should have done
enigma would have been effective if the VST itself checked the license:
bool VST_Init() {
char key[256], name[256];
EP_RegLoadKey(name, key);
if (!EP_RegCheckKey(name, key)) {
ShowTrialNag();
return false;
}
CreateThread(NULL, 0, LicenseWatchdog, NULL, 0, NULL);
return true;
}
instead, the VST has no EP_Reg* calls. no license checks. no callbacks. nothing. it just… runs.
the crack
the crack is embarrassingly simple. i spent hours analyzing RSA key formats for this…
the very sophisticated exploit
xcopy /E "C:\Program Files\Common Files\VST3\Bass Bully VST" .\crack\
copy "C:\ProgramData\Bass Bully VST\Bass Bully Premium\*.rom" .\crack\
that’s it. that’s the crack. copy the files. they work on any machine because there’s no license check in the actual product.
i wrote a python script to automate it because i have some self-respect:
#!/usr/bin/env python3
import shutil
from pathlib import Path
VST_SRC = Path(r"C:\Program Files\Common Files\VST3\Bass Bully VST\Bass Bully Premium.vst3")
ROM_SRC = Path(r"C:\ProgramData\Bass Bully VST\Bass Bully Premium\Bass Bully Premium.rom")
def extract():
out = Path("crack_package")
out.mkdir(exist_ok=True)
shutil.copytree(VST_SRC, out / "Bass Bully Premium.vst3", dirs_exist_ok=True)
shutil.copy2(ROM_SRC, out / "Bass Bully Premium.rom")
print("[+] done")
if __name__ == "__main__":
extract()
usage:
python patcher.py
load in fl studio. no registration. no nag. no nothing. because there’s no check.
for science: the hook approach
we also wrote a DLL that hooks enigma’s validation at runtime. completely unnecessary given the vulnerability, but i’d already done the research so here it is:
#include <windows.h>
#include <detours.h>
static int (WINAPI *Real_EP_RegCheckKey)(LPCSTR, LPCSTR) = NULL;
int WINAPI Hooked_EP_RegCheckKey(LPCSTR name, LPCSTR key) {
return 1;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
if (reason == DLL_PROCESS_ATTACH) {
Sleep(2000);
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)Real_EP_RegCheckKey, Hooked_EP_RegCheckKey);
DetourTransactionCommit();
}
return TRUE;
}
this approach works great. completely unnecessary. the payload has no protection.
lessons learned
for developers
-
protect the payload, not the installer - if users need the installed files to run your software, those files need runtime protection
-
defense in depth - don’t rely on a single layer. the VST should call
EP_RegCheckKeyon load -
threat model correctly - ask “what happens after installation?” if the answer is “nothing checks the license”, you have a problem
-
periodic validation - one-time checks are trivially bypassed by file copying
for reversers
-
always check the payload first - before diving into complex crypto, verify what you’re actually protecting
-
the simplest attack wins - don’t factor RSA when
xcopyworks -
protection != security - expensive protection systems are worthless if applied incorrectly
-
sometimes the crack writes itself - not every target requires sophisticated techniques
conclusion
enigma protector’s $250 protection was defeated by:
xcopy /E "C:\Program Files\..." .\crack\
the protection itself works fine - RSA signatures, HWID binding, anti-debug. but it only protects the installer. the payload runs completely unprotected.
250 dollars for a this…