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, 动态注入, 可视化界面, 图形钩子, 安卓, 模组菜单, 游戏外挂, 网络流量审计, 通知系统