No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-10 19:32:03 -04:00
.gitignore Redo CLI and flow 2026-08-10 18:39:41 -04:00
main.py Patch hv,lc,lsadm for ida-pro 2026-08-10 19:32:03 -04:00
README.md lumina 2026-08-10 19:00:13 -04:00

hexrays-patcher

A single-file keygen and binary patcher for IDA Pro 9.4 and its companion servers (license server, Hex-Vault / teams server, and Lumina).

It generates a self-signed licensing key and Certificate Authority, mints signed .hexlic license files, issues TLS server certificates, and patches the stock binaries so they trust your key and CA instead of Hex-Rays'. Everything is staged into out/ first; a generated install.sh performs the only step that needs root.

⚠️ For educational and interoperability research only. This tool reverse-engineers and modifies licensed software. Only use it on software you are authorized to modify, and comply with the applicable license terms.


Background — how it works

IDA's license check (reversed from sub_cd2e80 in libida.so) verifies a signed license blob with an RSA public key whose exponent is hardcoded to e = 0x13 (19) and whose modulus is stored obfuscated inside the binary. Because the check trusts whatever modulus is embedded in the binary, the scheme can be re-keyed end-to-end:

  1. Generate an e = 19 RSA key. OpenSSL can't generate this exponent, so the tool rolls its own primes (p, q with gcd(e, p-1) = 1).
  2. Sign licenses with it. The signature format and all of its padding parameters are derived from a SHA-256 digest over a salt + the exponent + the modulus. Rather than brute-force a modulus, the tool brute-forces the cheap 28-byte salt until the digest yields valid padding parameters.
  3. Patch the binaries so the stock (obfuscated) modulus → yours and the stock salt → yours. Now the binary verifies licenses your key signed.
  4. Replace the embedded TLS CA. Clients verify the license/vault servers' TLS certs against a Hex-Rays Root CA baked into the binary. The tool swaps that CA for its own and issues server leaf certs signed by it, so the self-hosted servers are trusted.

Two extra wrinkles the tool handles automatically:

  • Server licenses are bound to a host MAC. The LICENSE_SERVER / TEAMS_SERVER self-licenses embed the machine's interface MAC (the server matches it via getifaddrs). Generate on the host that will run the server.
  • Server certs are SAN-checked. The leaf certs cover localhost, ::1, 127.0.0.1, and this host's hostname/IPs. Add any other names clients use via HEX_EXTRA_SAN.

Requirements

  • uv — the script is a PEP 723 single-file script; uv fetches its one dependency (cryptography >= 50) automatically. No manual pip install.
  • Python 3.10+ (uv provides it if missing).
  • Linux (MAC/interface enumeration reads /sys/class/net).
  • Read access to the install directories (patching reads the stock binaries; it never writes to them). install.sh needs root to write under /opt.

Quick start

Run on the machine that will host the servers (for correct MAC binding / SAN):

# 1. Stage everything for every installed target (no root needed).
#    Uses default install locations; override with the env vars below.
uv run main.py

# 2. Review what was produced.
ls -R out/
out/install.sh --status          # preview exactly what would be installed

# 3. Install into the real directories (backs up originals). Needs root
#    because the servers live under /opt.
sudo out/install.sh

# 4. Start the license server / vault as usual — it now accepts your license.

main.py is executable, so ./main.py works too.

To target just one component:

uv run main.py --license-server
sudo out/install.sh license-server

Configuration (environment variables)

Variable Default Purpose
IDA_PRO_DIR ~/ida-pro-9.4 IDA Pro installation (the client).
HEX_LICSRV_DIR /opt/hexlicsrv License server installation.
HEX_VAULT_DIR /opt/hexvault Hex-Vault / teams server installation.
HEX_LUMINA_DIR /opt/lumina Lumina server installation (partial support).
HEX_STATE_DIR ./out Where the durable key + CA and the staging tree live.
HEX_EXTRA_SAN (empty) Comma-separated extra hostnames/IPs for server certs (e.g. licenses.corp.lan,10.0.0.5).

A target whose directory doesn't exist is skipped with a notice.


What gets produced (out/)

The patcher stages everything under HEX_STATE_DIR (default out/) and never touches the install directories itself:

out/
├── license.pem            # e=19 RSA signing key   (durable, reused across runs)
├── HexRaysCA.crt          # your Root CA           (durable)
├── HexRaysCA.key          # your Root CA key       (durable)
├── install.sh             # generated installer (copy → backup → chown/chmod)
├── ida-pro/
│   ├── idapro.hexlic
│   ├── libida.so          # patched copy, mirrors the install-dir path
│   └── MANIFEST
├── license-server/
│   ├── license_server_55-1337-DEAD-42.hexlic
│   ├── hexlicsrv.crt
│   ├── hexlicsrv.key
│   ├── license_server     # patched copy
│   └── MANIFEST
└── ...

license.pem and HexRaysCA.* are durable: generated once and reused on every subsequent run, so partial/incremental runs stay consistent (a license signed today verifies against a binary patched last week). Use --force to regenerate them from scratch (invalidates everything already installed).

Each MANIFEST is a tab-separated file the installer consumes:

type   relpath                       install destination                mode   owner
new    idapro.hexlic                 /home/you/ida-pro-9.4/idapro.hexlic  0644   -
patch  license_server                /opt/hexlicsrv/license_server        -      -
new    hexlicsrv.crt                 /opt/hexlicsrv/hexlicsrv.crt         0640   hexlicsrv:hexlicsrv
  • type=patch — a modified binary. The installer backs up the original to <dest>.bak (once) before overwriting; restorable.
  • type=new — a file the tool adds (license/cert/key). Removed on restore.
  • mode=- / owner=- — leave the destination's permission/owner as-is (this is how patched binaries keep their executable bit).

Targets

Target (--flag / installer name) Dir env License produced TLS cert Service user (chown, 640)
--ida-pro / ida-pro IDA_PRO_DIR idapro.hexlic (named) — (embeds your CA) — (world-readable 644)
--license-server / license-server HEX_LICSRV_DIR license_server_<lid>.hexlic hexlicsrv.crt/key hexlicsrv
--hex-vault / hex-vault HEX_VAULT_DIR teams_server_<lid>.hexlic hexvault.crt/key hexvault
--lumina / lumina HEX_LUMINA_DIR lumina_server_<lid>.hexlic lumina_server.crt/key lumina

For servers, the license/cert/key are installed owned by the service user and chmod 640 (the servers refuse to start if these are world-readable). If the service user doesn't exist, or install.sh isn't run as root, ownership is left unchanged and a warning is printed — the files still install.

Lumina note. lumina_server is patched (modulus/salt + CA) and gets a LUMINA_SERVER / lumina-server self-license bound to the host MAC; lc (the control client) only has its embedded CA patched. The TLS cert/key filenames (lumina_server.crt/key) match the paths in the shipped lumina.service — if you edited yours to use different -c/-k paths, rename accordingly.


Command-line reference

uv run main.py [options] [target ...]

Actions (default: do all phases for all present targets):

Option Effect
--generate-certs Only generate the crypto material (license.pem + HexRaysCA.*), then stop.
--no-licenses Don't generate .hexlic files.
--no-certs Don't generate TLS leaf certs.
--no-patch Don't stage patched binaries.
--status Report the state of the install dirs (read-only) and exit.
--dry-run Show what would be staged; write nothing.
--force Regenerate the RSA key + CA even if they already exist.
--state-dir DIR Override HEX_STATE_DIR.

Targets (choose a subset; omit to do all): --ida-pro, --license-server, --hex-vault, --lumina.

License customization (defaults < --license-template < flags):

Option Overrides
--license-template FILE JSON file of base license fields.
--owner NAME License owner (client license).
--email ADDR Payload email.
--name NAME Payload name.
--start-date "Y-M-D H:M:S" License start date.
--end-date "Y-M-D H:M:S" License end date.
--seats N Seats on the client (named IDA) license.

The --license-template JSON accepts any of these keys: name, email, owner, start_date, end_date, issued_on, seats.

uv run main.py --owner "Ada Lovelace" --email ada@example.com --end-date "2030-01-01 00:00:00"
# or from a file
uv run main.py --license-template mylicense.json

Installing, restoring, and status

The generated out/install.sh is self-contained — you can copy the whole out/ tree to another host and run it there.

sudo out/install.sh                      # install every staged product
sudo out/install.sh license-server hex-vault   # only these
out/install.sh --status                  # print planned actions, change nothing
sudo out/install.sh --restore            # undo: restore originals, remove added files
  • Install copies staged files into place, backing up each patched original to <file>.bak (once), then applies the manifest's chown/chmod.
  • Restore moves every .bak back over its binary and deletes the files the tool added (licenses/certs).

To check what's currently installed (looks for .bak backups):

uv run main.py --status

Re-running safely

Re-running the patcher is idempotent. When patching, it reads from the pristine <binary>.bak if one exists (left by a previous install), so re-patching an already-installed binary produces a correct result instead of double-patching. The durable key/CA are reused, so regenerated licenses keep matching the already-installed binaries.

If you change an install directory between staging and installing, re-stage — the manifest records absolute destinations computed at stage time.


Troubleshooting

  • The certificate file ".../hexlicsrv.crt" is world-accessible; exiting — install as root so install.sh can chown the cert/key to the service user and set mode 640. Running unprivileged leaves them owned by you; the server rejects that.
  • ... : pattern not found during patching — the stock modulus/salt wasn't in that file. Expected for admin-only tools like lsadm (they only need the CA patched), and for any binary that isn't the targeted version.
  • Server won't accept the license / MAC mismatch — the server license is bound to a network interface MAC. Regenerate on the actual server host.
  • TLS hostname errors from clients — add the name/IP the client dials to HEX_EXTRA_SAN and re-run, then reinstall the cert.

Technical notes

The heavy lifting (RSA e=19 keygen, the modulus/salt de-obfuscation keystreams, the digest-derived padding parameters, the signing routine, and the CA replacement) is documented inline at the top of main.py. Start there if you want to understand or extend the crypto.