Skip to content

Instantly share code, notes, and snippets.

@saagarjha
Created May 22, 2026 18:39
Show Gist options
  • Select an option

  • Save saagarjha/ae93a2a404ea8cec1acfc0bb0d85c9e5 to your computer and use it in GitHub Desktop.

Select an option

Save saagarjha/ae93a2a404ea8cec1acfc0bb0d85c9e5 to your computer and use it in GitHub Desktop.
Installs Binary Ninja given a license file
#!/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