gonidbg

module
v0.0.0-...-fb006b6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: Apache-2.0

README

简体中文 | English

gonidbg

CI Go Reference License

gonidbg 是 unidbg 的一个 Go 精简实现:在本机加载一个 Android AArch64 native 库(.so),不借助 JVM、真机或 Android 系统就能直接调用里面的函数。它给这个 .so 搭出一套够用的 Android 进程环境(动态链接器、真实的 bionic libc、一部分 Linux 系统调用、JNI/JavaVM),你就能从 Go 里调它的导出函数、读写它的内存。

和 unidbg 一样,CPU 引擎可以替换:Unicorn 解释器,或静态链接的 dynarmic JIT。编译时决定打包哪个,运行时决定用哪个。

e, _ := emulator.New(emulator.Config{SOPath: "libfoo.so"})
defer e.Close()
sum, _ := e.CallSymbol("add", 2, 3) // -> 5,作为真实 AArch64 代码执行

当前状态:两个引擎都能完整跑通,具体包括加载并链接 bionic 和目标 .so、执行 init_arrayJNI_OnLoad、调用导出函数、处理 syscall 与 JNI。它是 unidbg 的一个子集,缺哪些能力见 对标 unidbg


为什么

unidbg 是模拟 Android native 库的事实标准,但它跑在 JVM 上,依赖也偏重。gonidbg 想用 Go 把最核心的那部分重新做一遍:

  • 不需要 JVM。编译产物就是单个 Go 二进制,启动快、占用低。
  • 引擎可换。Unicorn 稳定,作为默认;dynarmic 是 JIT,热路径上快 5–9 倍;编译期或运行期都能选,跟 unidbg 的 backend 思路一致。
  • 复用真实 bionic。直接加载并模拟执行 AOSP sysroot 里的 libc/libm/libdl,省得自己重写一套 libc。
  • 代码量小。框架本体是几千行还算好读的 Go,外加两个很薄的 CPU 引擎 shim。

特性

  • AArch64 ELF 加载与动态链接(RELATIVE / JUMP_SLOT / GLOB_DAT / ABS64),DT_INIT + init_array
  • 复用真实 bionic libc/libm/libdl(内置 AOSP sdk23 sysroot),支持跨模块符号解析。
  • Linux/AArch64 系统调用子集(mmap/mprotect/openat/read/write/clock_gettime/getrandom/futex/…),配一套小型虚拟文件系统(/system/lib64/proc/self/*、属性、tzdata)。
  • JNI/JavaVM:guest 的 JNIEnv/JavaVM 调用会陷回到你用 Go 实现的处理器(FindClassGetMethodIDCall*Method*RegisterNatives、字符串、字节数组等)。
  • 按符号名或按模块偏移调用 native 函数,最多 8 个整型参数,可读取返回值。
  • 用 Go 回调替换 native 函数(Replace,入口 hook),或内联 hook(HookAddr,逐指令,Unicorn)改寄存器 / 重定向 PC;均自动让代码缓存失效。
  • 控制台调试器:断点 / 单步 / 寄存器 / 内存(Unicorn,I/O 可注入便于脚本化)。
  • classes.dex 加载真实类/方法/字段元数据(Config.DexPath / LoadDex):FindClass/GetMethodID/GetFieldID 按真实签名、父类解析(仅元数据,不执行字节码)。
  • 内存助手:分配、读写字节、C 字符串、小端整数。
  • 单指令 trace;以及完整指令流 trace(TraceInsns:每条指令的偏移 + 指令码 + 寄存器增量 + 调用/系统调用注解,Tenet 风格,可与真机 trace 对比;Unicorn)。
  • 引擎可选:-tags unicorn-tags dynarmic,或两个都编进去,运行期用 -engine / $GONIDBG_ENGINE 选择。

快速开始

前置条件
  • Go 1.24+
  • zig,需在 PATH 中,用作 cgo 的 C/C++ 交叉编译器,不需要 gcc 或 MSVC。
  • 一个 CPU 引擎:
    • Unicorn(默认):构建脚本会用 pip install unicorn 自动 vendoring。
    • dynarmic(可选,更快):跑一次 ./build-dynarmic.sh 完成 vendoring 和静态编译,详见 BUILD.md
构建并运行示例
# Windows (PowerShell)
powershell -ExecutionPolicy Bypass -File .\build.ps1            # -> bin\gonidbg.exe (unicorn)
.\bin\gonidbg.exe examples\native\native.so add 2 3            # add([2 3]) = 5

# Linux / macOS / git-bash
./build.sh
./bin/gonidbg examples/native/native.so fib 20                  # fib([20]) = 6765

完整演示(加载内置 native.so,调用导出函数、一个被 import 的 strlen、一个写指针的函数,以及一个 Go Replace hook):

go run -tags unicorn ./examples/run    # (cgo 环境变量见 BUILD.md;或直接用编好的二进制)
# engine: unicorn
# add(2, 3)      = 5
# fib(20)        = 6765
# slen(...)      = 14
# sum_into -> *out = 42
# add(2, 3) after Replace = 23  (Go hook: a*10+b)

作为库使用

import "github.com/sisi0318/gonidbg/emulator"

e, err := emulator.New(emulator.Config{
    SOPath:    "libfoo.so",        // 启动时加载并跑 init_array + JNI_OnLoad
    AssetRoot: emulator.Locate("assets"),
    Engine:    "",                 // "unicorn" | "dynarmic" | "" = 自动
})
if err != nil { panic(err) }
defer e.Close()

// 按名调用导出函数(最多 8 个整型/指针参数,返回 X0)。
r, _ := e.CallSymbol("add", 2, 3)

// 按模块偏移调用非导出入口(= unidbg 的 callFunction(offset))。
r, _ = e.CallOffset(nil /*主模块*/, 0x1234, argPtr)

// 交换内存。
p := e.WriteCStringAlloc("hello")
n, _ := e.CallSymbol("strlen_wrapper", p)
out := e.Malloc(4); _, _ = e.CallSymbol("sum_into", out, 20, 22)
v, _ := e.ReadU32(out)

// 用 Go 替换一个 native 函数(hook)。
e.ReplaceSymbol("add", func(h *emulator.Hook) uint64 { return h.Arg(0) + h.Arg(1) })
给 Java 侧建模(JNI)

native 库会通过 JNI 回调 Java。实现 dvm.Jni(或 embed dvm.AbstractJni,只重写你的库会用到的那几个方法),再传进 Config.JNI:

type MyJni struct{ dvm.AbstractJni }

func (MyJni) CallStaticObjectMethodV(vm *dvm.VM, cls *dvm.Class, sig string, va *dvm.VaList) *dvm.Object {
    if sig == "com/example/App->token()Ljava/lang/String;" {
        return &dvm.Object{Class: vm.ResolveClass("java/lang/String"), Value: "secret"}
    }
    return nil
}

e, _ := emulator.New(emulator.Config{SOPath: "libfoo.so", JNI: MyJni{}})

这就是 unidbg 里 AbstractJni 的用法:guest 的 RegisterNatives/GetMethodID/Call*Method 会按 "类->方法(签名)" 这样的字符串路由到你的 switch。

真实案例见 examples/douyin:用上面这套通用 API,在一个生产级混淆 .so 上复现请求签名头(该 .so 是第三方专有文件,不随仓库分发,需要自备)。

CPU 引擎

引擎 构建标签 链接方式 速度(热路径) 许可证
Unicorn2 -tags unicorn 运行时 dlopen libunicorn ~20 ms/次 GPLv2
dynarmic -tags dynarmic 静态链接(C++ via zig) ~2–4 ms/次 0BSD
  • 两个引擎可以编进同一个二进制(-tags "unicorn dynarmic"),运行期再选:gonidbg -engine dynarmic …GONIDBG_ENGINE=dynarmic
  • 每个模拟器第一次调用要花几百毫秒(dynarmic 现编 JIT,Unicorn 预热),之后复用同一个模拟器就很快了。
  • 许可证提示:Unicorn 是 GPLv2,静态链接它会让整个二进制都变成 GPLv2,所以 gonidbg 把它放在运行时 dlopen 的边界之后。dynarmic 是 0BSD(宽松许可),静态链接 dynarmic 不会带来 copyleft 牵连。dynarmic 的构建见 BUILD.md

工作原理

emulator.New 对照 unidbg Emulator 的启动流程:

  1. 地址空间:铺好 guest 栈、TLS(TPIDR_EL0 加一个 pthread_internal_t)和 SVC 跳板区,并选定 CPU 后端。
  2. 加载与链接:先处理真实 bionic 的 libc/libm/libdl,再处理你的 .so,也就是解析 ELF、映射段、处理重定位、跨模块解析符号;没解析到的 import 指向一个 svc 跳板,陷回 Go。
  3. 初始化:跑 DT_INITinit_array,如果导出了 JNI_OnLoad 也一并调用(传入合成的 JavaVM)。
  4. 调用:CallSymbol/CallOffset 把参数写进 X0..X7,把 LR 设成哨兵地址,然后一直跑到返回。SVC 陷入之后再分派给 syscall 层(internal/kernel)、JNI 层,或某个用 Go 实现的 libc 函数、被 Replace 的函数。

guest 的内存和寄存器通过 Backend 接口交换,两个引擎 shim 都实现了这个接口。dynarmic 后端给 JIT 提供了一张直接访存的页表,生成的代码可以直接读写宿主内存,只有遇到 SVC 才陷回 Go。

目录结构
gonidbg/
├── emulator/     公开 API:New、LoadLibrary、CallSymbol/CallOffset、Replace、内存助手
├── dvm/          公开:假 Dalvik VM —— VM、Object、Class、Jni、AbstractJni、VaList
├── internal/
│   ├── emu/      CPU 后端接口 + 注册表;unicorn(cgo)与 dynarmic(cgo/C++)shim
│   ├── loader/   ELF 解析 + 动态链接器
│   ├── kernel/   AArch64 Linux 系统调用子集
│   ├── memory/   guest 地址空间分配器
│   └── vfs/      guest 虚拟文件系统(/system/lib64、/proc/self、属性、tzdata)
├── cmd/
│   ├── gonidbg/  CLI:加载 .so 并调用某个符号
│   ├── elfscan/  分析 .so(导入/导出/init)
│   ├── loadplan/ 重定位直方图 / 链接复杂度
│   ├── bsmoke/   引擎自检
│   └── ucthread/ 最小引擎自检
├── examples/
│   ├── native/   一个自建的小 AArch64 .so(源码 + 预编译),供示例 + 测试用
│   └── douyin/   真实案例:在一个生产 .so 上复现签名(.so 需自备,不入库)
└── assets/android/sdk23/  内置 AOSP bionic sysroot(见 NOTICE)

对标 unidbg

已实现:AArch64 ELF 加载与动态链接 · 复用真实 bionic · 可选 Unicorn / dynarmic 后端 · Linux syscall 子集(含 uname/sysinfo/getdents64/readlinkat/statx/prlimit64/sched_getaffinity 等)· 带 Go 处理器的 JNI/JavaVM(字符串、字节/对象数组、异常、更多 Call 变体)· 按名或按偏移调用 · 函数 Replace内联 hook · 控制台调试器(断点/单步/寄存器/内存)· 从 classes.dex 加载真实类/方法/字段元数据 · 内存助手 · 指令 trace。

还没做的(路线图,欢迎 PR):

  • ARM32,目前只支持 AArch64。
  • JNI / syscall 仍是子集:覆盖常见用法,但不是全部 ~232 个 JNI 槽位 / 完整 syscall 表。
  • DEX 是元数据级:解析类/方法/字段(签名、父类)供 FindClass/GetMethodID/GetFieldID 解析,但不执行 DEX 字节码(无 JVM);Java 侧行为仍由你用 dvm.Jni 建模。
  • 内联 hook 与控制台调试器需 Unicorn 引擎(dynarmic 是块 JIT,无逐指令 hook)。
  • 真并发线程(已有单核协作式调度器:pthread_create 建 fiber、按时间片切换、在 futex/sleep 处保存并恢复 CPU 上下文 —— 但非真并发)、信号、iOS / Mach-O。

从源码构建 / 引擎

完整的工具链(用 zig 当 C/C++ 编译器)、纯 Go 层与引擎层、静态 dynarmic 构建(build-dynarmic.sh)以及 Windows/Linux 说明,都在 BUILD.md 里。

# 纯 Go 层随处可 build/test(无引擎、无 cgo):
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go test ./...

# 引擎集成测试(加载内置 native.so 并运行):
go test -tags unicorn  ./emulator
go test -tags dynarmic ./emulator

致谢与许可证

  • unidbg(Apache-2.0):本项目重新实现的对象。
  • Unicorn Engine(GPLv2):默认 CPU 后端,运行时加载。
  • dynarmic(0BSD):可选的 JIT CPU 后端。
  • AOSP bionic(Apache-2.0)等:assets/ 下内置的 sysroot,见 NOTICE

gonidbg 自身的代码采用 Apache-2.0(见 LICENSE)。引擎的许可证各不相同,见上表:Unicorn 后端走动态加载,把它的 GPLv2 限制在库边界之内;dynarmic 后端是宽松许可。

免责声明

gonidbg 是一个科研和教育用途的工具,用来分析你有权研究的 native 库。仓库里不含任何第三方应用的代码或专有二进制,只有一套通用的模拟框架,以及一个用本仓库源码自建的小示例库。请合理使用,并遵守适用的法律以及你所分析软件的相关条款。

Directories

Path Synopsis
cmd
elfscan command
elfscan: scope an AArch64 .so before loading it in gonidbg.
elfscan: scope an AArch64 .so before loading it in gonidbg.
loadplan command
loadplan: print the linker load plan for an ELF — segments, relocation histogram (linker complexity), import/export counts, init functions.
loadplan: print the linker load plan for an ELF — segments, relocation histogram (linker complexity), import/export counts, init functions.
Package dvm is the fake Dalvik/ART runtime: the JavaVM + JNIEnv the native library talks to.
Package dvm is the fake Dalvik/ART runtime: the JavaVM + JNIEnv the native library talks to.
Package emulator is gonidbg's high-level API: a minimal unidbg in Go.
Package emulator is gonidbg's high-level API: a minimal unidbg in Go.
internal
emu
Package emu defines the CPU backend abstraction — the single component that cannot be written in pure Go.
Package emu defines the CPU backend abstraction — the single component that cannot be written in pure Go.
kernel
Package kernel emulates the Linux ARM64 syscall interface reached via the SVC instruction.
Package kernel emulates the Linux ARM64 syscall interface reached via the SVC instruction.
loader
Package loader parses an Android ARM64 ELF shared object into a backend- agnostic load plan: segments to map, relocations to apply, symbols to export/import, and init functions to run.
Package loader parses an Android ARM64 ELF shared object into a backend- agnostic load plan: segments to map, relocations to apply, symbols to export/import, and init functions to run.
memory
Package memory manages the guest virtual address space: a page-granular region allocator backing the mmap/munmap/mprotect/brk syscalls and the loader's segment placement.
Package memory manages the guest virtual address space: a page-granular region allocator backing the mmap/munmap/mprotect/brk syscalls and the loader's segment placement.
vfs
Package vfs is the guest-visible filesystem.
Package vfs is the guest-visible filesystem.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL