0x41337/amm-oss

GitHub: 0x41337/amm-oss

一个基于 Rust 的 Android 游戏 Mod 菜单模板,通过自动 Hook 图形管线和触摸输入来渲染可扩展的 ImGui 覆盖层。

Stars: 0 | Forks: 0

# amm-oss 使用 Rust 编写的 Android Mod Menu 模板。通过 Hook 游戏的图形 pipeline 来渲染 ImGui 覆盖层,并拦截触摸输入来驱动它。旨在通过修改少量配置常量,即可针对不同的目标游戏进行适配。 ## 快速开始 ``` export ANDROID_NDK_HOME=$HOME/android-sdk/ndk/29.0.14033849 # 验证环境 make check # 构建 .so make build # 显示编译产物 make release ``` ## 目录 - [架构](#architecture) - [Mod API](#mod-api) - [配置系统](#configuration-system) - [注入](#injection) - [构建与开发体验 (Build & DX)](#build--dx) - [项目结构](#project-structure) - [依赖项](#dependencies) - [API 参考](#api-reference) ## 架构 `.so` 文件导出了 `JNI_OnLoad`,这是通过 `System.loadLibrary` 或 ptrace 注入器加载共享库的标准入口点。加载时,它会生成一个后台线程,等待游戏的库加载完成,然后安装 hooks。 ``` JNI_OnLoad └─ hack_thread() [after 200ms delay] ├─ init_config() → loads /data/local/tmp/amm-oss/config.json ├─ init_mods() → populates the mod registry ├─ menu::setup() │ ├─ gfx::setup() → auto-detects EGL or Vulkan, hooks the right entry point │ └─ input::setup() → hooks libinput.so (tries multiple mangled names) └─ wait for libil2cpp → resolve offset, store GET_MAIN Frame hook [every frame, depending on backend] ├─ EGL: eglSwapBuffers │ └─ frame_render() └─ Vulkan vkCreateSwapchainKHR (captures extent) └─ vkQueuePresentKHR └─ frame_render() frame_render() ├─ [first frame] setup_imgui() │ ├─ glow::Context from egl::get_proc_address │ ├─ imgui::Context (font_global_scale = 2.0) │ └─ AutoRenderer (imgui-glow-renderer) └─ [every frame] new_frame → draw_ui → render() libinput.so hooks [touch/motion events] ├─ initializeMotionEvent (API < 30, + vendor variants) └─ consume (API 30+, + vendor variants) └─ handle_motion_event → imgui::Io ``` ### 为什么要有两个图形后端? 游戏使用的是 OpenGL ES(通过 EGL)或 Vulkan。该模板会自动检测当前加载的是哪一个,并 hook 相应的函数: | 后端 | Hook 的函数 | 检测方式 | |---|---|---| | EGL | `eglSwapBuffers` | 已加载 `libEGL.so` | | Vulkan | `vkCreateSwapchainKHR` + `vkQueuePresentKHR` | 已加载 `libvulkan.so` | 对于 Vulkan,通过 hook `vkCreateSwapchainKHR` 来捕获交换链图像范围(宽/高),而 `vkQueuePresentKHR` 是主要的渲染 hook 点 —— 相当于 `eglSwapBuffers`。 目前,无论使用哪种后端,覆盖层都是通过 OpenGL ES 渲染的。对于 Vulkan 游戏,覆盖层会被渲染到离屏上下文中。有关如何显示它,请参阅 [Vulkan 注意事项](#vulkan-considerations)。 ### 为什么使用 libinput.so? Android 通过 `InputConsumer` 传递触摸事件。输入 hooks 会为每个函数尝试多个已知的 C++ 修饰名称,因为不同的 Android 版本和厂商构建版本会有所不同: | Hook | 尝试匹配 | 设备 | |---|---|---| | `initializeMotionEvent` | 3 个符号名称 | API < 30,厂商变体 | | `consume` | 4 个符号名称 | API 30+,厂商变体 | 这两个 hook 都会将事件作为鼠标事件(位置、按下/抬起、工具类型)传递给 `imgui::Io`。如果需要键盘输入,`input.rs` 中提供了一个被注释掉的 `initializeKeyEvent` 可作为起点。 ## Mod API 这是核心功能 —— 无需改动引擎即可添加 mod。 ### `MenuMod` trait 每个 mod 都实现了 `MenuMod`: ``` pub trait MenuMod: Send + Sync { /// Display name shown in the toggle list. fn name(&self) -> &str; /// Whether this mod is currently active. fn is_enabled(&self) -> bool; /// Called when the user toggles the mod on/off. fn set_enabled(&mut self, on: bool); /// Called every frame while the mod is enabled. /// Use `ui` to build any ImGui UI you want. fn draw(&mut self, ui: &mut imgui::Ui); } ``` `draw()` 方法**仅在启用该 mod 时运行**。如果你需要一个窗口,请在 `draw()` 中调用 `ui.window(...).build(|| { ... })`。如果你只想在内存中应用补丁(例如 God Mode),请将 `draw()` 留空并在 `set_enabled()` 中执行操作。 ### 注册表 (Registry) 一个全局的 `OnceLock>>>` 保存了所有的 mod。有两个函数会与它交互: ``` /// Add a mod to the registry. pub fn register_mod(m: Box); /// Run a closure with mutable access to the full mod list. pub fn with_mods(f: F) -> R where F: FnOnce(&mut Vec>) -> R; ``` 注册过程在 `init_mods()` 中进行,该方法会在启动且配置加载完成后调用一次。 ### draw_ui() 的工作原理 主菜单窗口 (`draw_ui`) 每一帧都会做三件事: 1. **收集 mod 信息** —— 从注册表中读取名称和启用状态(在 builder 闭包之外读取,以避免重复借用)。 2. **绘制切换列表** —— 每个 mod 对应一个复选框。切换状态时,会调用该 mod 的 `set_enabled()`。 3. **运行已启用的 mod** —— 遍历注册表,为每个启用的 mod 调用 `m.draw(ui)`。每个 mod 绘制各自的 UI(窗口、文本、滑块等)。 ### 创建你的第一个 mod **第 1 步。** 在你自己的文件中(或在 `init_mods()` 中)定义一个结构体: ``` pub struct GodMode { enabled: bool, } impl Default for GodMode { fn default() -> Self { Self { enabled: false } } } impl MenuMod for GodMode { fn name(&self) -> &str { "God Mode" } fn is_enabled(&self) -> bool { self.enabled } fn set_enabled(&mut self, on: bool) { self.enabled = on; if on { // apply memory patch } else { // restore original bytes } } fn draw(&mut self, _ui: &mut imgui::Ui) { // no UI — the toggle in the main menu is enough } } ``` **第 2 步。** 在 `src/menu/mod_api.rs` 的 `init_mods()` 中注册它: ``` pub fn init_mods() { register_mod(Box::new(GodMode::default())); register_mod(Box::new(FpsCounter::default())); } ``` 大功告成。该 mod 将自动出现在菜单中。 ### Mod 示例 `src/menu/examples.rs` 文件包含了三个被注释掉的示例: | Mod | 展示内容 | 适用场景 | |---|---|---| | **FpsCounter** | 读取 `ui.io().framerate`,显示 FPS 文本 | 学习 trait、调试覆盖层 | | **SpeedToggle** | 在 `draw()` 内部使用复选框 + 状态文本 | 需要自身子控件的 Mod | | **FloatSlider** | 在 `draw()` 内部使用 `ui.slider()` | 接收用户输入(范围、距离、乘数)的 Mod | 要启用它们,请取消 `init_mods()` 中相应行的注释: ``` crate::menu::examples::register_example_mods(); ``` ## 配置系统 启动时会加载 `/data/local/tmp/amm-oss/config.json`。 ``` { "game_lib": "libil2cpp.so", "offset_get_main": 6637460 } ``` | 字段 | 类型 | 默认值 | 用途 | |---|---|---|---| | `game_lib` | 字符串 | `"libil2cpp.so"` | 使用 `ElfScanner` 扫描的库名称 | | `offset_get_main` | 整数 | `0x27DC3B4` | 相对于库基址的函数偏移量 | 更改偏移量无需重新编译 —— 只需编辑 `config.json` 并重新注入即可。 ### 配置 API (`src/config.rs`) ``` /// Load config from disk. Returns Default on error/missing file. pub fn load_config() -> Config; /// Initialise the global CONFIG once at startup. pub fn init_config(); /// Run a closure with mutable access to the config. Returns None before init. pub fn with_config(f: F) -> Option where F: FnOnce(&mut Config) -> R; /// The resolved function address (lib_base + offset). pub static GET_MAIN: OnceLock usize>; ``` ## 注入 ### 静态注入 —— APK 内嵌(无需 root) 使用 `apktool` 反编译,在 smali 中找到主 Activity 的 `onCreate`,插入: ``` const-string v0, "menu" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V ``` 重新编译并签名。 | 优点 | 缺点 | |---|---| | 简单,易于理解 | 需要修改 APK | | 安装后无需 root | 破坏了原始签名 | | | 加载点可预测(更容易被检测) | | | 每次游戏更新都需要重新打包 | 无论 Rust 包名是什么,库名称都是 `menu` —— 参见 `Cargo.toml` 中的 `[lib] name = "menu"`。`loadLibrary("menu")` 会解析为 `libmenu.so`。 ### 动态注入 —— ptrace 注入(需要 root) [AndKittyInjector](https://github.com/0x41337/AndKittyInjector) 会附加到目标进程,并强制其在运行时加载我们的 `.so`。 ``` # 注入到运行中的进程 andkitty-injector \ --package com.target.game \ --libs /data/local/tmp/libmenu.so \ --memfd # 启动 app,等待 1s,注入,从 /proc/pid/maps 中隐藏 andkitty-injector \ --package com.target.game \ --libs /data/local/tmp/libmenu.so \ --launch --delay 1000000 --memfd --hide ``` | 优点 | 缺点 | |---|---| | 无需修改 APK | 需要 root 权限 (ptrace) | | 可在游戏启动后注入 | ptrace 是可被检测的 | | `--hide` 避免在 maps/soinfo 中可见 | | ### 注入方式对比 | 属性 | 静态 | 动态 | |---|---|---| | 需要 Root | 否 | 是 | | 修改 APK | 是 | 否 | | 检测面 | 签名,已知的 smali | ptrace,maps 条目 | | 游戏更新 | 必须重新打包 | 注入器保持不变 | | 启动控制 | 在游戏代码之前 | 可以延迟 | ## 构建与开发体验 (Build & DX) ### 构建 ``` cargo ndk build --release -t arm64-v8a ``` 要求: - [cargo-ndk](https://docs.rs/crate/cargo-ndk/latest) - Android NDK r27+ (`ANDROID_NDK_HOME`) - Rust target:`rustup target add aarch64-linux-android` 输出:`target/aarch64-linux-android/release/libmenu.so` 首次构建会从 GitHub 克隆 `kittymemory-rs`(git 依赖)。随后的构建将使用本地缓存。 ### Makefile Makefile 特意保持最简 —— 没有自动化流程,只有构建设置和验证: | 命令 | 操作 | |---|---| | `make check` | 验证 NDK、cargo-ndk 和 Rust target 是否就绪 | | `make build` | `check` + 构建发布版 .so | | `make release` | 显示已编译 `.so` 的路径和大小 | | `make fmt` | `cargo fmt` | | `make lint` | `cargo clippy` | 如果未设置 `ANDROID_NDK_HOME`,`make check` 将搜索常见路径(`$ANDROID_HOME/ndk/`、`$HOME/android-sdk/ndk/`、`$HOME/Android/Sdk/ndk/`)并建议正确的导出命令。 ``` # 典型工作流 export ANDROID_NDK_HOME=$HOME/android-sdk/ndk/29.0.14033849 make build make release # 检查格式和 lint make fmt make lint ``` ### rust-toolchain.toml 锁定 toolchain(稳定版)并确保已安装 Android target。此外还锁定了 2024 edition,这是首批使用该 edition 的 Rust 项目之一 —— 如果 toolchain 拒绝它,请更新 rustc。 ### Release profile ``` strip = true lto = true panic = "abort" opt-level = "s" (optimise for size) ``` ## 项目结构 ``` rust-toolchain.toml Toolchain pin + Android target Makefile Build, check, fmt, lint config.json Runtime configuration (pushed to device) build.rs Links libc++_static.a + libc++abi.a (for dobby-rs) src/ lib.rs JNI_OnLoad entry point, hack_thread lifecycle config.rs Config struct (serde), JSON load/save, GET_MAIN menu/ mod.rs Re-exports: init_mods, register_mod, MenuMod, with_mods, setup, render gfx.rs Graphics backend: auto-detect EGL/Vulkan, hook eglSwapBuffers or vkCreateSwapchainKHR + vkQueuePresentKHR mod_api.rs MenuMod trait + Registry (register_mod, with_mods, init_mods) state.rs MenuState (imgui::Context + AutoRenderer in OnceLock>), SHOW_DEMO AtomicBool, INIT flag hooks.rs Thin shim: delegates to gfx::setup() + input::setup_input_hooks() draw.rs draw_ui() (main menu window, mod toggle list, enable iteration) and render() (new_frame → draw_ui → AutoRenderer::render) input.rs libinput.so hooks (initializeMotionEvent / consume), tries multiple mangled names per hook point, feeds touch events into imgui::Io examples.rs Example MenuMod impls: FpsCounter, SpeedToggle, FloatSlider (registrations commented out by default) ``` ### 文件职责 | 文件 | 职责 | |---|---|---| | `lib.rs` | 生成 `hack_thread`,初始化 config → mods → hooks,等待游戏库加载,解析偏移量 | | `config.rs` | 运行时 `Config` 结构体,JSON 持久化,`GET_MAIN` 函数指针 | | `gfx.rs` | 自动检测 EGL/Vulkan 后端,安装正确的 hooks,共享帧渲染 | | `mod_api.rs` | `MenuMod` trait 定义,全局 `REGISTRY`,注册/生命周期 | | `state.rs` | 线程安全的 imgui 上下文 + 渲染器,延迟初始化标志,demo 切换 | | `hooks.rs` | 代理 gfx + 输入设置的薄垫片 | | `draw.rs` | 帧 pipeline,主菜单窗口,mod 切换迭代,渲染 | | `input.rs` | 带有多个符号回退机制的触摸/动作 Hook,ImGui 输入桥接 | | `examples.rs` | 供学习 API 之用的参考实现 | ## 依赖项 | Crate | 用途 | |---|---| | `imgui` | UI 框架 —— 窗口、按钮、文本、滑块、复选框 | | `imgui-glow-renderer` | 通过 glow 以 OpenGL ES 渲染 ImGui 绘制数据 | | `egl` | EGL 绑定 —— 用于显示尺寸的 `query_surface` | | `jni` | JNI 类型 (`JNI_VERSION_1_6`, `jint`) | | `ndk` / `ndk-sys` | Android NDK —— `AInputEvent`, `MotionEvent`, `InputConsumer` | | `dobby-rs` | 运行时 Hook —— `resolve_symbol`, `hook` | | `kittymemory-rs` | 内存扫描 —— `ElfScanner::find` | | `android_logger` / `log` | Logcat 输出 | | `glow` | OpenGL ES 函数加载器(基于 `egl::get_proc_address` 构建) | | `serde` / `serde_json` | 配置序列化 (`config.json`) | ## API 参考 ### `pub use menu::*` | 符号 | 来源 | 描述 | |---|---|---|---| | `init_mods()` | `mod_api.rs` | 启动时调用一次。在此处注册你的 mod。 | | `register_mod(Box)` | `mod_api.rs` | 将 mod 添加到全局注册表 | | `with_mods(f)` | `mod_api.rs` | 从任意位置访问 mod 列表 | | `MenuMod` (trait) | `mod_api.rs` | `name()`, `is_enabled()`, `set_enabled()`, `draw()` | | `setup()` | `hooks.rs` | 安装图形 + 输入 hooks | | `gfx::detect_backend()` | `gfx.rs` | 根据已加载的库返回 `Egl` 或 `Vulkan` | | `gfx::screen_size()` | `gfx.rs` | 当前显示尺寸(由活动后端设置) | | `render(w, h)` | `draw.rs` | 单帧:new_frame → draw_ui → renderer | ### `config` 模块 | 符号 | 描述 | |---|---| | `Config { game_lib, offset_get_main }` | 可序列化的运行时配置 | | `CONFIG_PATH` | `/data/local/tmp/amm-oss/config.json` | | `LOG_TAG` | `"ModMenu"` —— 用于 logcat 过滤 | | `init_config()` | 从磁盘加载配置,初始化全局变量 | | `load_config()` → `Config` | 读取 + 反序列化(出错时返回默认值) | | `with_config(f)` → `Option` | 在闭包内部修改配置 | | `GET_MAIN` | 解析过的 `lib_base + offset` 函数指针 | ### `state` 模块 | 符号 | 描述 | |---|---| | `MenuState { context, renderer }` | 包含 `imgui::` + `imgui_glow_renderer::AutoRenderer` | | `MENU_STATE` | `OnceLock>` —— 全局线程安全状态 | | `SHOW_DEMO` | `AtomicBool` —— 切换 ImGui demo 窗口 | | `INIT` | `OnceLock` —— 第一次 imgui 初始化后变为 true | | `with_menu_state(f)` | 在闭包内部访问 imgui 上下文 + 渲染器 | ### 生命周期总结 ``` Process start │ ├─ JNI_OnLoad(vm, key) │ ├─ init android_logger │ └─ spawn hack_thread │ └─ hack_thread (background) ├─ sleep(200ms) ├─ config::init_config() ← reads config.json ├─ menu::init_mods() ← registers mods ├─ menu::setup() │ ├─ gfx::setup() ← detects EGL or Vulkan │ │ ├─ EGL: hook eglSwapBuffers │ │ └─ Vulkan hook vkCreateSwapchainKHR + vkQueuePresentKHR │ └─ input::setup_input_hooks() ← tries multiple symbol names │ ├─ [loop] wait for libil2cpp │ └─ resolve offset → GET_MAIN │ └─ [every frame: backend hook] ├─ [first] state::INIT → setup_imgui() ├─ draw::render(w, h) │ ├─ context.new_frame() │ ├─ draw_ui() │ │ ├─ [for each registered mod] checkbox toggle │ │ │ └─ on toggle: set_enabled() │ │ └─ [for each enabled mod] mod.draw(ui) │ └─ renderer.render() └─ original function (eglSwapBuffers or vkQueuePresentKHR) ``` ### Vulkan 注意事项 当游戏使用 Vulkan 时,覆盖层渲染仍然通过 OpenGL ES(经由 `glow`)进行。这之所以可行,是因为 Android 支持在同一个进程中同时存在 EGL 和 Vulkan。覆盖层的尺寸是通过 `vkCreateSwapchainKHR` hook 从 `VkSwapchainCreateInfoKHR::imageExtent` 中捕获的。 为了让覆盖层在使用 Vulkan 时能实际显示在屏幕上,你需要以下方法之一: - **EGL 回退**:如果游戏同时也初始化了 EGL(许多 Unity 游戏即使在使用 Vulkan 时也会这么做),覆盖层将直接渲染。 - **离屏 + 合成**:渲染到 pbuffer surface,然后通过 Vulkan 命令将结果合成到交换链图像上。 - **覆盖视图**:使用 JNI 在游戏窗口之上创建一个透明的 `SurfaceView`,并渲染到其 EGL surface 上。 该模板提供了 hook 脚手架;具体的合成方法取决于目标游戏的设置。 ## 许可证 本项目基于 MIT 许可证授权。
标签:ImGui, Rust, 动态注入, 可视化界面, 图形钩子, 安卓, 模组菜单, 游戏外挂, 网络流量审计, 通知系统