diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b22edfd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + python-version: "3.12" + - run: uv sync + - run: uv run ruff check src/ + - run: uv run ruff format --check src/ diff --git a/README.md b/README.md index 550d883..b11855e 100644 --- a/README.md +++ b/README.md @@ -47,26 +47,26 @@ Optional (for `analyze`): | [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) | IL2CPP symbol extraction | | `strings` | Native `.so` string extraction | -Optional Python extras: - -```bash -uv pip install "enma[unity]" # UnityPy for asset extraction -``` - --- ## Installation ```bash -git clone https://github.com/yourname/enma +git clone https://github.com/ykus4/enma cd enma uv sync ``` -Install git hooks for development: +Activate the virtual environment (optional — prefix commands with `uv run` instead): + +```bash +source .venv/bin/activate +``` + +For Unity asset extraction, install the optional extra: ```bash -uv run pre-commit install +uv pip install "enma[unity]" ``` --- @@ -212,6 +212,8 @@ uv run enma repack app.apk -o app-gadget.apk --arch arm64 | `--arch` | `arm64` (default) / `arm` / `x86_64` / `x86` | | `--keep-workdir` | Keep the intermediate smali directory | +Requires on `PATH`: `apktool`, `zipalign`, and `apksigner` (Android build-tools) or `jarsigner` (JDK). + Pipeline: 1. Download `frida-gadget-{ver}-android-{arch}.so.xz` → cached in `~/.cache/enma/` 2. `apktool d` — decode APK @@ -230,11 +232,7 @@ uv run enma unity uv run enma unity ./dump -o ./extracted ``` -Requires `UnityPy`: - -```bash -uv pip install "enma[unity]" -``` +Requires the `enma[unity]` extra (see [Installation](#installation)). Extracts from every `.unity3d` bundle in the dump directory: @@ -569,6 +567,7 @@ enma/ ├── bypass/ # ssl, crypto, anti_detect, anti_tamper, safetynet ├── network/ # http, websocket, protobuf, binder ├── storage/ # sqlite, fileio, dlopen + ├── ue4/ # ue4_sdk, ue4_pak, ue4_blueprint └── mem/ # memscan, mempatch ``` diff --git a/docs/architecture.md b/docs/architecture.md index b8689e3..4238d7c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -13,7 +13,7 @@ enma is a two-process system: a **Python host** running on the analyst's machine │ Analyst Machine │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ enma CLI (Python) │ │ +│ │ enma CLI (Python) │ │ │ │ │ │ │ │ cli.py ──► device.py (frida-server deploy) │ │ │ │ ──► repack.py (APK gadget injection) │ │ @@ -21,6 +21,7 @@ enma is a two-process system: a **Python host** running on the analyst's machine │ │ ──► analyze.py (post-dump analysis) │ │ │ │ ──► report.py (HTML report generation) │ │ │ │ ──► unity.py (AssetBundle extraction) │ │ +│ │ ──► ue4.py (UE4 runtime analysis) │ │ │ └────────────────────────┬────────────────────────────────────┘ │ │ │ Frida RPC / message bus │ │ │ (USB / TCP) │ @@ -37,15 +38,15 @@ enma is a two-process system: a **Python host** running on the analyst's machine │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Target App Process │ │ │ │ │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ -│ │ │ Java VM │ │ ART / JIT│ │ Native │ │ libc / │ │ │ -│ │ │ (Dalvik) │ │ (libart) │ │ Libs │ │ BoringSSL│ │ │ -│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ -│ │ │ │ │ │ │ │ -│ │ └─────────────┴─────────────┴──────────────┘ │ │ -│ │ ▲ │ │ -│ │ Frida Interceptors │ │ -│ │ (JS agents injected) │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Java VM │ │ ART / JIT│ │ Native │ │ libc / │ │ │ +│ │ │ (Dalvik) │ │ (libart) │ │ Libs │ │ BoringSSL│ │ │ +│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ +│ │ │ │ │ │ │ │ +│ │ └─────────────┴─────────────┴──────────────┘ │ │ +│ │ ▲ │ │ +│ │ Frida Interceptors │ │ +│ │ (JS agents injected) │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` @@ -54,7 +55,7 @@ enma is a two-process system: a **Python host** running on the analyst's machine ## Agent Categories -The 20 agents are grouped into 5 functional categories, each in its own subdirectory. +The 23 agents are grouped into 6 functional categories, each in its own subdirectory. ``` agents/ @@ -89,6 +90,11 @@ agents/ │ ├── fileio_agent.js File open/write/delete monitoring │ └── dlopen_agent.js Dynamic library load monitoring │ +├── ue4/ ← Unreal Engine 4 runtime analysis +│ ├── ue4_sdk_agent.js GNames/GUObjectArray dump → SDK JSON (UE4.23–4.27) +│ ├── ue4_pak_agent.js PAK file discovery and dump +│ └── ue4_blueprint_agent.js Blueprint ProcessEvent call tracer +│ └── mem/ ← Interactive memory manipulation ├── memscan_agent.js CheatEngine-style value scanner └── mempatch_agent.js Write / NOP / freeze memory @@ -209,6 +215,7 @@ enma mempatch com.example.game 0x7ff1a2b4 -t int32 -v 99999 │ "unity" ──► run_unity() ◄── unity.py │ │ "setup" ──► run_setup() ◄── device.py │ │ "repack" ──► run_repack() ◄── repack.py │ +│ "ue4" ──► run_ue4() ◄── ue4.py │ │ "list" ──► list_apps() │ └──────────────────────────────────────────────────────────────────┘ @@ -327,7 +334,10 @@ Each agent operates at one or more of the following hook layers: │ Layer 2: Native libraries │ │ Interceptor.attach(Module.findExportByName("libssl.so", …)) │ │ Interceptor.attach(Module.findExportByName("libcrypto.so", …)) │ -│ → ssl (BoringSSL), crypto (EVP_*), dlopen (libdl) │ +│ Interceptor.attach(Module.findExportByName("libUE4.so", …)) │ +│ → ssl (BoringSSL), crypto (EVP_*), dlopen (libdl), │ +│ ue4_sdk (GNames/GUObjectArray), ue4_pak (FPakFile), │ +│ ue4_blueprint (ProcessEvent) │ ├─────────────────────────────────────────────────────────────────┤ │ Layer 1: libc / syscall │ │ Interceptor.attach(Module.findExportByName("libc.so", "open")) │ diff --git a/src/enma/repack.py b/src/enma/repack.py index c30dc7d..9dc357b 100644 --- a/src/enma/repack.py +++ b/src/enma/repack.py @@ -84,14 +84,22 @@ def _ensure_debug_keystore(keystore: Path) -> None: logger.info("Generating debug keystore ...") keytool_args = [ "-genkeypair", - "-keystore", str(keystore), - "-alias", "androiddebugkey", - "-keyalg", "RSA", - "-keysize", "2048", - "-validity", "10000", - "-storepass", "android", - "-keypass", "android", - "-dname", "CN=Android Debug,O=Android,C=US", + "-keystore", + str(keystore), + "-alias", + "androiddebugkey", + "-keyalg", + "RSA", + "-keysize", + "2048", + "-validity", + "10000", + "-storepass", + "android", + "-keypass", + "android", + "-dname", + "CN=Android Debug,O=Android,C=US", ] _run("keytool", *keytool_args) @@ -137,20 +145,28 @@ def _sign_apk(signer: str, signer_path: str, src: Path, dest: Path, keystore: Pa _run( signer_path, "sign", - "--ks", str(keystore), - "--ks-pass", "pass:android", - "--key-pass", "pass:android", - "--ks-key-alias", "androiddebugkey", - "--out", str(dest), + "--ks", + str(keystore), + "--ks-pass", + "pass:android", + "--key-pass", + "pass:android", + "--ks-key-alias", + "androiddebugkey", + "--out", + str(dest), str(src), ) else: shutil.copy(src, dest) _run( signer_path, - "-keystore", str(keystore), - "-storepass", "android", - "-keypass", "android", + "-keystore", + str(keystore), + "-storepass", + "android", + "-keypass", + "android", str(dest), "androiddebugkey", )