Created
May 22, 2026 18:39
-
-
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" | |
| 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. license → serial + key blob | |
| with open(args.license) as f: | |
| lic = json.load(f)[0] | |
| serial = lic["serial"] | |
| key_hex = base64.b64decode(lic["data"])[:256].hex() | |
| # 2. /update handshake — `arch`, `osmajor`, `distro`, `NAME` are decorative, | |
| # the server ignores them. `platform` is the only one that matters. | |
| qs = urllib.parse.urlencode({ | |
| "product": "Binary Ninja", "serial": serial, "key": key_hex, | |
| "version": "0.0.0", "platform": args.platform, | |
| }) | |
| update = json.loads(http_get(f"{MASTER}/update?{qs}")) | |
| cdn = update["url"].rstrip("/") | |
| trust = next(k for k in update["keys"] if k["name"] == "binaryninja-v1") | |
| print(f"[*] {trust['name']} → {cdn}") | |
| # 3. /manifest/binaryninja-v1 → channels list | |
| channels = json.loads(unwrap_envelope( | |
| http_get(f"{cdn}/manifest/{trust['name']}"), | |
| bytes.fromhex(trust["key"]) | |
| ))["channels"] | |
| channel = next(c for c in channels if c["name"] == args.channel) | |
| # 4. /channel/<channel> → versions list (newest first) | |
| 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