- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .gitignore | ||
| main.py | ||
| README.md | ||
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:
- Generate an
e = 19RSA key. OpenSSL can't generate this exponent, so the tool rolls its own primes (p,qwithgcd(e, p-1) = 1). - 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.
- Patch the binaries so the stock (obfuscated) modulus → yours and the stock salt → yours. Now the binary verifies licenses your key signed.
- 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_SERVERself-licenses embed the machine's interface MAC (the server matches it viagetifaddrs). 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 viaHEX_EXTRA_SAN.
Requirements
- uv — the script is a
PEP 723 single-file script; uv fetches its
one dependency (
cryptography >= 50) automatically. No manualpip 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.shneedsrootto 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_serveris patched (modulus/salt + CA) and gets aLUMINA_SERVER/lumina-serverself-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 shippedlumina.service— if you edited yours to use different-c/-kpaths, 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'schown/chmod. - Restore moves every
.bakback 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 soinstall.shcanchownthe cert/key to the service user and set mode640. Running unprivileged leaves them owned by you; the server rejects that.... : pattern not foundduring patching — the stock modulus/salt wasn't in that file. Expected for admin-only tools likelsadm(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_SANand 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.