tl;dr

i spent a day analyzing enigma protector, a $200 commercial software-protection system used by thousands of vendors. RSA signatures, hardware-bound licensing, anti-debugging and VM-based code obfuscation. serious enterprise security theater.

then i noticed that the protected installer extracts a completely unprotected payload to disk.

xcopy /E "C:\Program Files\...\product" .\crack\

that is the entire crack. copy the installed files. they run on another machine without a keygen, binary patch or cryptanalysis.

$200 protection defeated by a command that shipped with DOS 3.2 in 1986.

the problem was not the cryptography. it was the threat model.


target overview

the target was bass bully premium, a VST3 synthesizer plugin protected by enigma protector. the commercial packer costs $250 or more and promises protection against copying, modification and analysis.

bass bully premium landing page

the installer presented a hardware-bound registration flow before releasing the product.

bass bully premium protected installer

i had one known valid license:

Key:  GLUJ-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-V99KP3
HWID: 3148CC-XXXXXX
Name: Bass Bully

the goal was to understand the protection and build a proper crack.


static analysis

the PE headers

first, i checked what i was 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

the entry point at 0x16485D0 sits unusually deep in the binary; a common sign of a packed executable whose original entry point is hidden. normal PE files usually begin execution much closer to 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.

does it 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, ...

there was no winhttp.dll, wininet.dll or ws2_32.dll. validation appeared to happen offline, which meant the key material and verification path had to exist locally in the binary.


the enigma protector internals

enigma protector provides four relevant layers:

  1. code virtualization: transforms x86 or x64 code into proprietary VM bytecode
  2. anti-debugging: uses checks such as IsDebuggerPresent, timing checks and hardware-breakpoint detection
  3. anti-tampering: validates packed sections with CRC checks
  4. registration API: binds licenses to an HWID and verifies RSA signatures

its own documentation explains the intended protection model in detail.

enigma protector deployment and protection model

the protection model works only when the actual program module performs the checks. that distinction became important later.

the registration API

according to the SDK, protected applications can use functions such as:

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 are not normal exports. enigma resolves them dynamically after unpacking, so an external tool cannot simply retrieve them with GetProcAddress. bypassing them normally requires a runtime hook or a pattern scan of unpacked memory.

the entry point

i used 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 jump-over-garbage pattern is classic anti-disassembly. a linear disassembler decodes the bytes between the jump and its target as instructions, while the real unpacker begins at 0x1416485DA with a register-preservation sequence.


key format analysis

i had a known valid key, so i started by mapping its structure:

GLUJ-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-V99KP3

the key contains eight dash-separated groups. groups zero through six contain four base36 characters; the final group contains six.

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

the final group was significantly larger, which made it look like a truncated signature. the apparent structure was:

[    DATA: ~143 bits     ] [ SIGNATURE: 31 bits ]
 groups 0-6                 group 7

enigma uses RSA for signing. validation would therefore involve an embedded public key and a signature check; a keygen would require recovering the relevant parameters or attacking the verification path.

the HWID contained two 24-bit values, producing a 48-bit hardware fingerprint:

hwid = "3148CC-059521"
parts = hwid.split('-')

the protection itself looked credible. a license generated for one machine should not work on another. the problem was where that protection ended.


the pivot

at this point i was preparing either to recover the RSA parameters or hook validation at runtime. then i stopped and asked a more useful question: what is the installer actually protecting?

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
  ...

the enigma imports were missing.

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

the same result appeared through ordinary string searches:

strings "Bass Bully Premium.vst3" | grep -i enigma
strings "Bass Bully Premium.vst3" | grep -i regist

no output.

the VST was a clean JUCE build. it contained no enigma runtime, registration callbacks or other license checks.

they had protected the installer, not the product.


the vulnerability

the entire protection stack controlled whether the installer could run. once the files reached disk, the protection stopped mattering.

+-------------------------------------------------------------------+
|                    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  <- no protection          |  | |
|  |  |  - Bass Bully Premium.rom   <- not encrypted          |  | |
|  |  +--------------------------------------------------------+  | |
|  +--------------------------------------------------------------+ |
+-------------------------------------------------------------------+

it was a vault door attached to a tent.

what should have happened

enigma would have been useful if the VST itself had 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;
}

the actual VST had no EP_Reg* calls, license checks or callbacks. it simply loaded.


the crack

after hours spent mapping the key format, the working attack was two copy commands:

xcopy /E "C:\Program Files\Common Files\VST3\Bass Bully VST" .\crack\
copy "C:\ProgramData\Bass Bully VST\Bass Bully Premium\*.rom" .\crack\

the copied files worked on another machine because the product itself never checked the license.

i also wrapped the same operation in python:

#!/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()
python patcher.py

no registration, nag screen or binary modification was required.


for science: the hook approach

i had already written a DLL to hook enigma's validation at runtime. it was unnecessary for this target, but it confirmed that the original reverse-engineering path was viable.

#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;
}

runtime hook injected into the protected installer

the hook worked. the unprotected payload made it irrelevant.


what to take away from this

the expensive protection mechanisms were real: RSA signatures, HWID binding, anti-debugging, anti-tampering and code virtualization. they were simply applied to the wrong component.

for developers, protect the payload rather than only the installer. perform license checks inside the software that users actually run, validate periodically and assume that installed files can be copied.

for reversers, inspect the payload before attacking the packer. verify where the trust boundary ends, because the simplest valid attack is usually better than the most technically interesting one.

do not factor RSA when xcopy works.