{ "cells": [ { "cell_type": "markdown", "id": "400a2588", "metadata": {}, "source": [ "# Black Bird APK patcher for Google Colab\n", "\n", "Installs tools, configure the target server, upload an APK, patch it for a private server, rebuild, sign, and download the patched APK.\n", "\n", "**Patches**\n", "- `global-metadata.dat` — rewrite IL2CPP string literals (URLs and hostnames)\n", "- `libil2cpp.so` — ARM64 binary patches (SSL bypass, encryption passthrough, plain Octo list)\n", "- `AndroidManifest.xml` — add `networkSecurityConfig` for cleartext HTTP (when using HTTP)\n", "- `res/xml/network_security_config.xml` — allow cleartext traffic\n", "\n", "**Quick Instructions**\n", "- Configure `protocol`, `server_host`, and optional `server_port` below.\n", "- Run the single code cell. It installs tools, patches, and downloads the result.\n", "\n", "**Notes**\n", "- Choose `http` to enable cleartext network patches; choose `https` to skip manifest/network changes.\n", "- Replacements must not be longer than the original strings; use short hostnames or omit the port if needed." ] }, { "cell_type": "code", "execution_count": null, "id": "combined-cell", "metadata": {}, "outputs": [], "source": [ "# ── Configuration ────────────────────────────────────────────────────────────\n", "protocol = 'http' #@param ['http', 'https']\n", "server_host = '192.168.1.1' #@param {type:'string'}\n", "server_port = '' #@param {type:'string'}\n", "\n", "# ── Imports & constants ───────────────────────────────────────────────────────\n", "import os\n", "import re\n", "import shutil\n", "import stat\n", "import struct\n", "import subprocess\n", "import urllib.parse\n", "from dataclasses import dataclass\n", "from pathlib import Path\n", "\n", "WORK_ROOT = Path('/content/nier_patch_work')\n", "TOOLS_DIR = WORK_ROOT / 'tools'\n", "ANDROID_SDK_ROOT = TOOLS_DIR / 'android-sdk'\n", "APKTOOL_VERSION = '3.0.1'\n", "ANDROID_BUILD_TOOLS = '36.1.0'\n", "ANDROID_CMDLINE_TOOLS_URL = 'https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip'\n", "\n", "METADATA_MAGIC = 0xFAB11BAF\n", "HDR_STRING_LITERAL_OFF = 8\n", "HDR_STRING_LITERAL_DATA_OFF = 16\n", "\n", "MOV_X0_0 = struct.pack(' Path:\n", " path.mkdir(parents=True, exist_ok=True)\n", " return path\n", "\n", "# ── Tool installation ─────────────────────────────────────────────────────────\n", "def install_apktool():\n", " ensure_dir(TOOLS_DIR)\n", " jar_path = TOOLS_DIR / f'apktool_{APKTOOL_VERSION}.jar'\n", " wrapper_path = TOOLS_DIR / 'apktool'\n", "\n", " if not jar_path.exists():\n", " run([\n", " 'wget', '-q', '-O', str(jar_path),\n", " f'https://github.com/iBotPeaches/Apktool/releases/download/v{APKTOOL_VERSION}/apktool_{APKTOOL_VERSION}.jar',\n", " ])\n", "\n", " wrapper_path.write_text(\n", " '#!/usr/bin/env bash\\n'\n", " f'exec java -jar \"{jar_path}\" \"$@\"\\n',\n", " encoding='utf-8',\n", " )\n", " wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC)\n", " os.environ['PATH'] = f'{TOOLS_DIR}:{os.environ[\"PATH\"]}'\n", "\n", "def install_android_commandline_tools() -> Path:\n", " ensure_dir(TOOLS_DIR)\n", " ensure_dir(ANDROID_SDK_ROOT)\n", "\n", " zip_path = TOOLS_DIR / 'commandlinetools.zip'\n", " temp_dir = TOOLS_DIR / 'cmdline-tools-temp'\n", " cmdline_root = ANDROID_SDK_ROOT / 'cmdline-tools'\n", " latest_dir = cmdline_root / 'latest'\n", "\n", " if not latest_dir.exists():\n", " if temp_dir.exists():\n", " shutil.rmtree(temp_dir)\n", " run(['wget', '-q', '-O', str(zip_path), ANDROID_CMDLINE_TOOLS_URL])\n", " ensure_dir(temp_dir)\n", " run(['unzip', '-q', str(zip_path), '-d', str(temp_dir)])\n", " ensure_dir(cmdline_root)\n", " extracted_dir = temp_dir / 'cmdline-tools'\n", " if latest_dir.exists():\n", " shutil.rmtree(latest_dir)\n", " shutil.move(str(extracted_dir), str(latest_dir))\n", "\n", " return latest_dir\n", "\n", "def install_android_build_tools():\n", " latest_dir = install_android_commandline_tools()\n", " sdkmanager = latest_dir / 'bin' / 'sdkmanager'\n", "\n", " env = os.environ.copy()\n", " env['ANDROID_SDK_ROOT'] = str(ANDROID_SDK_ROOT)\n", " env['JAVA_HOME'] = '/usr/lib/jvm/java-17-openjdk-amd64'\n", " env['PATH'] = f'{latest_dir / \"bin\"}:{env[\"PATH\"]}'\n", "\n", " run(f'yes | \"{sdkmanager}\" --sdk_root=\"{ANDROID_SDK_ROOT}\" --licenses >/dev/null', check=False, shell=True, env=env)\n", " run([\n", " str(sdkmanager),\n", " f'--sdk_root={ANDROID_SDK_ROOT}',\n", " f'build-tools;{ANDROID_BUILD_TOOLS}',\n", " 'platform-tools',\n", " ], env=env)\n", "\n", " build_tools_dir = ANDROID_SDK_ROOT / 'build-tools' / ANDROID_BUILD_TOOLS\n", " os.environ['ANDROID_SDK_ROOT'] = str(ANDROID_SDK_ROOT)\n", " os.environ['PATH'] = f'{build_tools_dir}:{latest_dir / \"bin\"}:{os.environ[\"PATH\"]}'\n", "\n", "def install_dependencies():\n", " ensure_dir(WORK_ROOT)\n", " ensure_dir(TOOLS_DIR)\n", "\n", " run(['apt-get', 'update'])\n", " run([\n", " 'apt-get', 'install', '-y',\n", " 'openjdk-17-jdk-headless',\n", " 'wget',\n", " 'unzip',\n", " 'ca-certificates',\n", " ])\n", "\n", " install_apktool()\n", " install_android_build_tools()\n", "\n", " missing = [tool for tool in ('apktool', 'apksigner', 'zipalign', 'keytool') if shutil.which(tool) is None]\n", " if missing:\n", " raise RuntimeError(f'Missing required tools after installation: {missing}')\n", "\n", " print('\\nInstalled tool paths:')\n", " for tool in ('apktool', 'apksigner', 'zipalign', 'keytool'):\n", " print(f' {tool}: {shutil.which(tool)}')\n", "\n", " print('\\nRequested versions:')\n", " print(f' apktool: {APKTOOL_VERSION}')\n", " print(f' Android build-tools: {ANDROID_BUILD_TOOLS}')\n", "\n", "# ── Patch helpers ─────────────────────────────────────────────────────────────\n", "@dataclass\n", "class RuntimeConfig:\n", " protocol: str\n", " host: str\n", " port: str = ''\n", "\n", " @property\n", " def web_base_url(self) -> str:\n", " base = f'{self.protocol}://{self.host}'\n", " if self.port:\n", " base += f':{self.port}'\n", " return base\n", "\n", " @property\n", " def grpc_host(self) -> str:\n", " return self.host\n", "\n", "def require_file(path: Path):\n", " if not path.is_file():\n", " raise FileNotFoundError(f'Not found: {path}')\n", "\n", "def build_replacements(cfg: RuntimeConfig):\n", " replacements = [\n", " ('api.app.nierreincarnation.com', cfg.grpc_host),\n", " (\n", " 'https://web.app.nierreincarnation.com/assets/release/{0}/database.bin',\n", " f'{cfg.web_base_url}/assets/release/{{0}}/database.bin',\n", " ),\n", " ('https://web.app.nierreincarnation.com', cfg.web_base_url),\n", " ('https://resources-api.app.nierreincarnation.com/', f'{cfg.web_base_url}/'),\n", " ]\n", "\n", " for old_value, new_value in replacements:\n", " if len(new_value.encode('utf-8')) > len(old_value.encode('utf-8')):\n", " raise ValueError(\n", " 'Replacement too long: '\n", " f'{old_value!r} -> {new_value!r}. '\n", " 'Use a shorter host name or omit the port.'\n", " )\n", "\n", " return replacements\n", "\n", "def patch_metadata_strings(meta_path: Path, replacements) -> int:\n", " data = bytearray(meta_path.read_bytes())\n", "\n", " magic = struct.unpack_from(' {len(new_bytes)}')\n", " break\n", "\n", " if not entry_found:\n", " print(f' [!] No table entry found for {old_str!r} (dataIndex=0x{data_index:X})')\n", " continue\n", "\n", " data[blob_pos:blob_pos + len(old_bytes)] = new_bytes + (b'\\x00' * (len(old_bytes) - len(new_bytes)))\n", " print(f' PATCHED: {old_str!r} -> {new_str!r}')\n", " patched += 1\n", "\n", " meta_path.write_bytes(data)\n", " return patched\n", "\n", "def patch_manifest(manifest_path: Path) -> bool:\n", " text = manifest_path.read_text(encoding='utf-8')\n", "\n", " if 'networkSecurityConfig' in text:\n", " print(' networkSecurityConfig already present')\n", " return True\n", "\n", " new_attr = 'android:networkSecurityConfig=\"@xml/network_security_config\"'\n", " patched_text, count = re.subn(r' tag in AndroidManifest.xml')\n", "\n", " manifest_path.write_text(patched_text, encoding='utf-8')\n", " print(f' added {new_attr}')\n", " return True\n", "\n", "NETWORK_SECURITY_CONFIG = '''\n", "\n", " \n", "\n", "'''\n", "\n", "def create_network_security_config(res_xml_dir: Path) -> bool:\n", " res_xml_dir.mkdir(parents=True, exist_ok=True)\n", " output_path = res_xml_dir / 'network_security_config.xml'\n", " output_path.write_text(NETWORK_SECURITY_CONFIG, encoding='utf-8')\n", " print(f' wrote {output_path}')\n", " return True\n", "\n", "def patch_libil2cpp(so_path: Path) -> int:\n", " patched = 0\n", " with so_path.open('r+b') as handle:\n", " file_size = handle.seek(0, 2)\n", " for patch in IL2CPP_PATCHES:\n", " rva = patch['rva']\n", " patch_bytes = patch['bytes']\n", " if rva + len(patch_bytes) > file_size:\n", " print(f\" [!] SKIP {patch['name']}: RVA 0x{rva:X} beyond file size\")\n", " continue\n", "\n", " handle.seek(rva)\n", " original = handle.read(len(patch_bytes))\n", " handle.seek(rva)\n", " handle.write(patch_bytes)\n", "\n", " patched += 1\n", " print(f\" {patch['name']} @ 0x{rva:X}: {original.hex()} -> {patch_bytes.hex()}\")\n", " print(f\" {patch['desc']}\")\n", "\n", " return patched\n", "\n", "def ensure_debug_keystore() -> Path:\n", " keystore_path = TOOLS_DIR / 'debug.keystore'\n", " if keystore_path.exists():\n", " return keystore_path\n", "\n", " run([\n", " 'keytool',\n", " '-genkeypair',\n", " '-v',\n", " '-keystore', str(keystore_path),\n", " '-storepass', 'android',\n", " '-alias', 'androiddebugkey',\n", " '-keypass', 'android',\n", " '-keyalg', 'RSA',\n", " '-keysize', '2048',\n", " '-validity', '10000',\n", " '-dname', 'CN=Android Debug,O=Android,C=US',\n", " '-noprompt',\n", " ])\n", " return keystore_path\n", "\n", "def patch_uploaded_apk(uploaded_apk: Path, cfg: RuntimeConfig) -> Path:\n", " job_dir = WORK_ROOT / 'job'\n", " decoded_dir = job_dir / 'decoded'\n", " unsigned_apk = job_dir / f'{uploaded_apk.stem}-unsigned.apk'\n", " aligned_apk = job_dir / f'{uploaded_apk.stem}-aligned.apk'\n", " signed_apk = job_dir / f'{uploaded_apk.stem}-patched.apk'\n", "\n", " if job_dir.exists():\n", " shutil.rmtree(job_dir)\n", " job_dir.mkdir(parents=True, exist_ok=True)\n", "\n", " print(f'[*] Decompiling {uploaded_apk.name} ...')\n", " run(['apktool', 'd', '-f', str(uploaded_apk), '-o', str(decoded_dir)])\n", "\n", " metadata_path = decoded_dir / 'assets/bin/Data/Managed/Metadata/global-metadata.dat'\n", " so_path = decoded_dir / 'lib/arm64-v8a/libil2cpp.so'\n", " manifest_path = decoded_dir / 'AndroidManifest.xml'\n", " res_xml_dir = decoded_dir / 'res/xml'\n", "\n", " for required_path in (metadata_path, so_path, manifest_path):\n", " require_file(required_path)\n", "\n", " replacements = build_replacements(cfg)\n", "\n", " print(f'\\n[*] Patching for server {cfg.host} (web={cfg.web_base_url}, gRPC host={cfg.grpc_host})')\n", "\n", " print('\\n[1] Patching global-metadata.dat string literals ...')\n", " patched_strings = patch_metadata_strings(metadata_path, replacements)\n", " print(f' {patched_strings}/{len(replacements)} strings patched')\n", "\n", " print('\\n[2] Patching libil2cpp.so ...')\n", " patched_methods = patch_libil2cpp(so_path)\n", " print(f' {patched_methods}/{len(IL2CPP_PATCHES)} methods patched')\n", "\n", " if cfg.protocol == 'http':\n", " print('\\n[3] Enabling cleartext traffic in AndroidManifest.xml ...')\n", " patch_manifest(manifest_path)\n", "\n", " print('\\n[4] Writing network_security_config.xml ...')\n", " create_network_security_config(res_xml_dir)\n", " else:\n", " print('\\n[3] HTTPS selected; skipping cleartext network security changes')\n", "\n", " print('\\n[5] Rebuilding APK ...')\n", " run(['apktool', 'b', str(decoded_dir), '-o', str(unsigned_apk)])\n", "\n", " print('\\n[6] Aligning APK ...')\n", " run(['zipalign', '-f', '4', str(unsigned_apk), str(aligned_apk)])\n", "\n", " print('\\n[7] Signing APK ...')\n", " keystore_path = ensure_debug_keystore()\n", " if signed_apk.exists():\n", " signed_apk.unlink()\n", " shutil.copy2(aligned_apk, signed_apk)\n", " run([\n", " 'apksigner', 'sign',\n", " '--ks', str(keystore_path),\n", " '--ks-pass', 'pass:android',\n", " '--key-pass', 'pass:android',\n", " '--ks-key-alias', 'androiddebugkey',\n", " str(signed_apk),\n", " ])\n", "\n", " print('\\n[8] Verifying signature ...')\n", " run(['apksigner', 'verify', '--verbose', str(signed_apk)])\n", "\n", " print(f'\\n[+] Finished: {signed_apk}')\n", " return signed_apk\n", "\n", "# ── Validate config ───────────────────────────────────────────────────────────\n", "server_host = server_host.strip()\n", "server_port = server_port.strip()\n", "\n", "if not server_host:\n", " raise ValueError('server_host cannot be empty')\n", "\n", "if server_port and not server_port.isdigit():\n", " raise ValueError('server_port must be blank or numeric')\n", "\n", "display_port = f':{server_port}' if server_port else ''\n", "print(f'Configured web base URL: {protocol}://{server_host}{display_port}')\n", "print(f'Configured gRPC host patch: {server_host}')\n", "\n", "# ── Install dependencies ──────────────────────────────────────────────────────\n", "install_dependencies()\n", "\n", "# ── Download, patch, and export APK ──────────────────────────────────────────\n", "apk_url = 'https://archive.org/download/nierreincarnation/Global/apk/NieR%20Re%5Bin%5Dcarnation%203.7.1.apk'\n", "output_name = Path(urllib.parse.unquote(apk_url.split('/')[-1]))\n", "input_apk = WORK_ROOT / output_name.name\n", "ensure_dir(WORK_ROOT)\n", "\n", "print(f'Downloading APK from {apk_url} ...')\n", "run(['wget', '-q', '-O', str(input_apk), apk_url])\n", "if not input_apk.exists():\n", " raise RuntimeError('Download failed')\n", "print(f'Downloaded {input_apk.name} ({input_apk.stat().st_size:,} bytes)')\n", "\n", "cfg = RuntimeConfig(protocol=protocol, host=server_host, port=server_port)\n", "output_apk = patch_uploaded_apk(input_apk, cfg)\n", "print(f'Patched APK: {output_apk}')\n", "\n", "try:\n", " from google.colab import files\n", " files.download(str(output_apk))\n", "except Exception:\n", " print('Browser download failed; patched APK is at:', output_apk)" ] } ], "metadata": { "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }