Last active
August 17, 2026 18:10
-
-
Save saagarjha/ae93a2a404ea8cec1acfc0bb0d85c9e5 to your computer and use it in GitHub Desktop.
Installs Binary Ninja given a license file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Install Binary Ninja from license.dat alone. Pure stdlib + `openssl` CLI. | |
| bn_install.py LICENSE TARGET [--channel dev|release|test] | |
| [--platform linux-arm|linux|macosx|win64] | |
| Each layer is the same primitive — IV(16) || AES-128-CBC(zlib(payload)) — and | |
| hands you the AES key for the next. /manifest and /channel additionally have a | |
| 256-byte RSA-PSS signature prefix. | |
| """ | |
| import argparse, base64, hashlib, json, os, stat, subprocess, sys | |
| import urllib.parse, urllib.request, zlib | |
| MASTER = "https://master.binary.ninja" | |
| UA = "Binary Ninja/installer" | |
| # User-facing channels mapped to concrete channel names, in preference order. | |
| # The update server decides which manifests a license may access; we discover | |
| # those manifests first, then choose the most capable matching channel. | |
| CHANNELS = { | |
| "dev": ["dev-enterprise", "dev", "dev-personal", "headless-dev"], | |
| "release": ["enterprise", "release", "release-personal", "headless"], | |
| "test": ["test-enterprise", "test"], | |
| } | |
| def aes_cbc(key, iv, ct): | |
| """AES-128-CBC decrypt by shelling out to `openssl`. `-nopad` because our zlib | |
| stream is followed by trailing PKCS#7-style padding that zlib happily ignores.""" | |
| p = subprocess.run( | |
| ["openssl", "enc", "-aes-128-cbc", "-d", "-nopad", | |
| "-K", key.hex(), "-iv", iv.hex()], | |
| input=ct, capture_output=True, check=True) | |
| return p.stdout | |
| def unwrap_envelope(blob, key): | |
| """RSA-sig(256) || IV(16) || AES-CBC(zlib(content)).""" | |
| return zlib.decompress(aes_cbc(key, blob[256:272], blob[272:])) | |
| def unwrap_chunk(blob, key): | |
| """IV(16) || AES-CBC(zlib(content)).""" | |
| return zlib.decompress(aes_cbc(key, blob[:16], blob[16:])) | |
| def http_get(url): | |
| req = urllib.request.Request(url, headers={"User-Agent": UA}) | |
| with urllib.request.urlopen(req) as r: | |
| return r.read() | |
| def main(): | |
| ap = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, | |
| description=__doc__) | |
| ap.add_argument("license", help="path to license.dat") | |
| ap.add_argument("target", help="target install directory") | |
| ap.add_argument("--channel", default="dev", choices=["dev", "release", "test"]) | |
| ap.add_argument("--platform", default="linux-arm", choices=["linux-arm", "linux", "macosx", "win64"]) | |
| args = ap.parse_args() | |
| # 1-4. Try every license entry. Like the real client, /update returns the | |
| # manifests/roles available to that license; each manifest returns channels. | |
| # Collect them all, then choose the best concrete channel for the requested | |
| # user-facing channel. | |
| with open(args.license) as f: | |
| licenses = json.load(f) | |
| if isinstance(licenses, dict): | |
| licenses = [licenses] | |
| available = {} | |
| for lic in licenses: | |
| serial = lic["serial"] | |
| key_hex = base64.b64decode(lic["data"])[:256].hex() | |
| qs = urllib.parse.urlencode({ | |
| "product": lic.get("product", "Binary Ninja"), | |
| "serial": serial, | |
| "key": key_hex, | |
| "version": "0.0.0", | |
| "platform": args.platform, | |
| }) | |
| update = json.loads(http_get(f"{MASTER}/update?{qs}")) | |
| if not update.get("ok"): | |
| continue | |
| cdn = update["url"].rstrip("/") | |
| for trust in update["keys"]: | |
| channels = json.loads(unwrap_envelope( | |
| http_get(f"{cdn}/manifest/{trust['name']}"), | |
| bytes.fromhex(trust["key"]) | |
| ))["channels"] | |
| for channel in channels: | |
| # Match the real client's behavior: de-duplicate by channel name. | |
| available.setdefault(channel["name"], (cdn, trust, channel)) | |
| selected = None | |
| for channel_name in CHANNELS[args.channel]: | |
| selected = available.get(channel_name) | |
| if selected: | |
| break | |
| if not selected: | |
| wanted = ", ".join(CHANNELS[args.channel]) | |
| found = ", ".join(sorted(available)) or "none" | |
| sys.exit(f"no usable channel found for {args.channel!r} (wanted {wanted}; found {found})") | |
| cdn, trust, channel = selected | |
| print(f"[*] {trust['name']}/{channel['name']} → {cdn}") | |
| versions = json.loads(unwrap_envelope( | |
| http_get(f"{cdn}/channel/{channel['name']}"), | |
| bytes.fromhex(channel["key"]) | |
| ))["versions"] | |
| version = versions[0] | |
| build = version["builds"][args.platform]["release"] | |
| print(f"[*] installing {version['version']} ({args.platform})") | |
| # 5. /download/<chunk-manifest-sha> → file index | |
| cm = json.loads(unwrap_chunk( | |
| http_get(f"{cdn}/download/{build['object']}"), | |
| bytes.fromhex(build["key"]) | |
| )) | |
| bundle_sha = next(e["bundle"]["object"] for e in cm if isinstance(e.get("bundle"), dict)) | |
| print(f"[*] {len(cm)} entries; fetching bundle ({bundle_sha[:16]}…)") | |
| # 6. /download/<bundle> — the only large transfer | |
| bundle = http_get(f"{cdn}/download/{bundle_sha}") | |
| if hashlib.sha256(bundle).hexdigest() != bundle_sha: | |
| sys.exit("bundle SHA mismatch") | |
| # 7. extract everything into target | |
| os.makedirs(args.target, exist_ok=True) | |
| for i, ent in enumerate(cm, 1): | |
| if not isinstance(ent, dict): continue | |
| out = os.path.join(args.target, ent["path"]) | |
| os.makedirs(os.path.dirname(out) or ".", exist_ok=True) | |
| if ent.get("type") == "link": | |
| if os.path.lexists(out): os.unlink(out) | |
| os.symlink(ent.get("link") or ent.get("target"), out) | |
| else: | |
| b = ent["bundle"] | |
| pt = unwrap_chunk(bundle[b["offset"] : b["offset"]+b["size"]], | |
| bytes.fromhex(ent["key"])) | |
| if hashlib.sha256(pt).hexdigest() != ent["object"]: | |
| sys.exit(f"plaintext SHA mismatch for {ent['path']}") | |
| with open(out, "wb") as f: f.write(pt) | |
| if ent.get("exec"): | |
| os.chmod(out, os.stat(out).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) | |
| if i % 500 == 0: | |
| print(f" [{i}/{len(cm)}] {ent['path']}") | |
| print(f"[*] DONE → {args.target}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment