首页
社区
课程
招聘
[原创]手搓 JniForward:Unidbg JNI 转发真实 Android ART 的探索
发表于: 11小时前 335

[原创]手搓 JniForward:Unidbg JNI 转发真实 Android ART 的探索

11小时前
335

论坛里偶尔能看到「SO 在 PC 模拟、JNI 丢真机跑」的说法,当时没看太懂中间怎么接。后来手写补环境补烦了,自己撸了一版。

下面先简单说下整体思路,再写实现细节和踩坑。

分工:PC 跑 SO,手机跑该真算的那部分 Java

对象跨进程:只传编号,不传指针

举个短例子(SO 遍历 TreeMap):

要点:PC 的 DvmObject 多半是「指向手机的遥控器」;手机 Handle 表里才是 TreeMapIterator 本体。同一条链里编号对得上就行,PC 的 1003 和手机的 1003 不要求是同一个内存对象。

手机算完之后怎么传回来?按返回值类型来

一次 JNI = 一次 JSON 往返。手机 invoke 完,按 JNI 返回值是什么类型,在 JSON 里带不同的 result,不是固定只传 Handle:

对象还要链式调用(nextgetKey),就靠 Handle 来回指;字符串、字节数组 把值传回来就行,SO 在 Native 里直接用,通常不必再登记成 Handle。

分流:不是全部 JNI 都上网

本质都是:PC 截 JNI → 发请求 → 真机 ART 执行 → 结果回传。差别在执行端放哪、要少写多少补环境。

Router 在架构里干什么(和上面对应)

Router 本身不算 JNI、也不连手机,只做三件事:

分拣之后是两条执行链,互斥、只走一条

所以架构图里「代理 → Router → 分叉」就是:先进 Router 再决定本地算还是手机算;控制台 [local] / [remote] 就是这次走了哪条叉。

Native 指令只在 PC;需要真 JVM 的 JNI 才走右边那条叉。

可以把它想成快递分拣中心:SO 每次调 JNI,都先到 Router,Router 不自己算,只负责「这单该本地送还是发手机」。

Unidbg 原来直接调 AbstractJni。我在外面包了一层动态代理:任何 callObjectMethodfindClass……先进代理,代理只做一件事:

这样不用改 Unidbg 源码,用户还是 vm.setJni(this)

Router 收到「方法名 + 参数」,压进一个小结构:

op 比如 "callObjectMethodV"args 里是 [vm, dvmObject, signature, varArg]
为什么要打包? 后面发 JSON 时,整包 JniCall 转 args 就行,Router 本身不关心本地还是远程。

真正「分发」发生在 HybridJniExecutor

路由策略怎么判? 从参数里找出 JNI 的 signature 字符串(形如 java/util/Iterator->next()Ljava/lang/Object;):

判完以后,两条路:

本地路LocalJniExecutor 用反射找到 Jni 接口上对应方法,调原来的实现类——和没加转发前一模一样,补环境 switch 还写在这。

远程路RemoteJniExecutorJniCall 编成 JSON,Socket 发到 8765,等手机回一行 JSON,再解码塞回 Unidbg。

外面再包一层「打印执行器」,控制台就会看到:

[remote] / [local] 就是 Router 分发结果的直观体现。

一句话:Router = 统一进门 → 打包成 JniCall → 按类名前缀分拣 → 本地反射 or 远程 JSON。

第 1 步:先想直接改 AbstractJni —— 不行

最开始很直接的想法,把 Unidbg 里每个 JNI override 改成一行转发,例如:

findClasscallBooleanMethod 等几十个方法都要这么改。能跑,但:

后来改成 不动 AbstractJni,外面用动态代理包 Jni 接口,再只做 Router + 本地反射,跑通 SO 确认和改之前结果一致。

第 2 步:定 JSON 协议 + PC 端假 Agent

在 PC 再起一个监听 8765 的小程序,用真 Java 处理 TreeMap/Iterator,跟未来手机 Agent 同一套 JSON。协议定成 一行 JSON 一问一答,方便 log 里直接 grep。

遇到的问题:Unidbg 回调名带 V,Agent 只认不带 V 的 op

Unidbg 的 Jni 接口里,处理可变参数的方法名末尾会多一个 **V**(表示 VarArg),比如 callObjectMethodVcallBooleanMethodV。我在 PC 侧用动态代理转发时,method.getName() 拿到的就是这个带 V 的名字,原样写进 JSON 的 op 字段。

假 Agent 分发器是按「不带 V 的标准名」写的 switch,两边对不上:

表现就是:Socket 通了、JSON 也 parse 成功,但 Agent 回 ok:false,或直接抛 unknown op: callObjectMethodV。跟参数翻译无关,纯粹是 op 名字不一致。PC 发 JSON 前,把末尾的 V 剥掉即可:

这样 PC 和 Agent 只维护一套 op 分支,不用为每个 xxxV 再复制一份。

第 3 步:参数翻译(最难,具体代码 AI 帮着改了几轮)

Unidbg 里 SO 调 JNI 时,参数不是普通 Java 对象,而是一堆框架类型:DvmObjectVarArgVaListStringObject……PC 要把这些「翻译成 JSON 能发的形式」再发出去,手机算完还要「翻译回来」塞给 Unidbg。我主要理规则(什么发 HANDLE、什么发 MAP),具体反射和编码 AI 帮着改了几轮。

第 4 步:分流策略 —— 为什么「全扔手机」不行

跑通 PC 假 Agent 之后,很自然地想:既然真机 ART 算得准,干脆所有 JNI 都发手机,PC 一个 switch 都不用写。

试下来不行,原因是:独立 Agent 是一个自己安装的 APK,不在目标 App 进程里。

所以分流不能是「能发就发」,而是 按类名前缀划边界

第 5 步:真机 Agent APK

前面 PC 假 Agent 就是在电脑上开了一个「小型 Java 服务」,监听 8765,收到 JSON 就调 TreeMap、回 JSON。第五步做的事很简单:把这段逻辑原样搬进手机里。

可以把它想成在手机上装了一个「专职接电话的 App」:

第 6 步:删 java/ 补环境
TreeMap/Iterator 的 override 全删,只留 vm.setJni(this) 和 android/* 伪装,签名和纯 PC 跑一致才算通。

本地处理的是「Router 分拣错了或不该上网的」,最终仍进本地 switch 补环境。

壳的类型不能设成占位符类本身,必须按 signature 推断真实 Java 类型,否则后面 JNI 类型全乱。

手机 Agent 与 PC MockAgent 的 JniOpDispatcher 采用 按 JNI signature 硬编码 的 if/switch:收到 JSON 后根据 op + signature 在真机执行对应 Java 调用。未实现的 signature 会在 logcat 中报 UnsupportedOperationException,再按需增加分支。

当前仅针对示例 SO 已实现的远程调用(Map 遍历签名链,非框架全集):

receiver:参数为 MAP 时在手机 new TreeMap<>(map) 并登记编号;为 HANDLE 时查表取对象。换其他 SO 时,Router/JSON 可复用,Agent 需按实际 java/* 调用扩展上述列表。

请求idopargs(数组,每项 type+value

成功idok:trueresultresultType(小写 handle/boolean/bytes/string)

失败ok:falseexceptionmessage

类型:NULL、BOOL、I32、STRING、HANDLE(大写)、BYTES(Base64)、MAP

收发(一行一问一答):

以前:几十行补 TreeMap/Iterator。

现在

工程脚本与 快速上手.md 一致,在项目根目录分步执行。

成功标志:

图片描述

窗口 A(保持运行):

看到 listening on 127.0.0.1:8765 即可。

窗口 B(依次两条):

成功标志:
图片描述

日志里应有 JNI >> [remote] ...

【图3】

窗口 A

图片描述
窗口 B

成功标志:pong + 签名 70dc3d08... + JNI >> [remote](来自手机)。

图片描述

完整工程见 jni-forward-unidbg(仅供参考:环境、机型、示例 SO 各异,不一定能直接跑通,主要提供实现思路与目录结构)。

期望由手机计算的 独立 Agent 里实际会怎样
Application.getPackageName() 返回 Agent 自己的包名(比如 com.example.jniagent),不是目标 App
getPackageManager().getPackageInfo(...) 查的是 Agent 能看到的包,签名校验、版本号全不对
com.xxx.business.EncryptUtil 等业务类 Agent 的 ClassLoader 根本加载不到目标 APK 里的类
Context.getAssets() / getResources() 拿到的是 Agent 的资源,不是目标 App 的 so、配置、证书
需要目标进程内存里的单例、静态字段 完全不在一个进程,查表也查不到
PC(Unidbg)
  libxxx.so 在 Unicorn 里跑
  SO 调 JNI
    ↓
  【代理层】拦住所有 Jni 回调,统一进 Router
    ↓
  【Router.dispatch】打包成一次 JniCall(方法名 + 参数)
    ↓
  【路由策略】看 signature 前缀
    ├─ java/*、javax/*  ──► 【远程执行器】编 JSON → adb → 手机
    └─ android/*、业务类  ──► 【本地执行器】反射调 AbstractJni 补环境

手机 Agent
  127.0.0.1:8765 收 JSON → 真机 invoke → 回一行 result(HANDLE/BOOL/BYTES…)
JniCall
  → 路由:isRemote?
       否 → LocalJniExecutor → 反射调 AbstractJni(补环境 switch)
       是 → RemoteJniExecutor → JSON 往返 → JniArgBridge 译回 DvmObject/boolean/bytes
return router.dispatch(方法名, 参数数组);
public Object dispatch(String op, Object... args) {
    return executor.execute(new JniCall(op, args));
}
public Object execute(JniCall call) throws Throwable {
    if (!路由策略.isRemote(call)) {
        return 本地执行器.execute(call);   // 还是调 AbstractJni
    }
    try {
        return 远程执行器.execute(call);   // 发 JSON 到手机
    } catch (IOException e) {
        if (允许回退) return 本地执行器.execute(call);
        throw e;
    }
}
JNI >> [remote] callObjectMethod java/util/Iterator->next()Ljava/lang/Object;
JNI >> [local]  callObjectMethod android/app/Application->getPackageManager()...
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> o,
                                     String signature, VarArg varArg) {
    return (DvmObject<?>) router.dispatch("callObjectMethod", vm, o, signature, varArg);
}
// PC 侧:代理里 method.getName() 拿到的名字
request.setOp("callObjectMethodV");   //  JSON 里 op 带 V 不行

// Agent 侧
switch (req.getOp()) {
    case "callObjectMethod":   // 只注册了这条
        return invokeObjectMethod(req);
    default:
        throw new UnsupportedOperationException("unknown op: " + req.getOp());
}
private static String normalizeOp(String op) {
    if (op.endsWith("V") && op.length() > 1) {
        return op.substring(0, op.length() - 1);
    }
    return op;
}
// callObjectMethodV  → callObjectMethod
// callBooleanMethodV → callBooleanMethod
// 包在 AbstractJni 外面
return (Jni) Proxy.newProxyInstance(
    Jni.class.getClassLoader(),
    new Class<?>[]{ Jni.class },
    (proxy, method, args) -> router.dispatch(method.getName(), args)
);
public Object execute(JniCall call) throws Throwable {
    // 方法名 + 参数数量 + 参数类型在 Jni 接口里找 Method
    Method method = resolveMethod(call.getOp(), call.getArgs());
    return method.invoke(delegate, call.getArgs());
}
public Object execute(JniCall call) throws Throwable {
    BaseVM vm = 从参数里取出vm;
    RpcValue[] args = 把JniCall参数翻译成JSON可发的形式;
    String op = 归整方法名(call.getOp());  // callObjectMethodV → callObjectMethod
    RpcRequest request = new RpcRequest(请求序号++, op, args);

    try (Socket socket = new Socket(host, port)) {
        写出一条JSON加换行;
        读回一行JSON;
        return 把JSON结果变回Unidbg能用的返回值(vm, response, signature);
    }
}
// DvmObject 里如果是 远程编号 占位→ 发 HANDLE
if (value instanceof RemoteHandleMarker) {
    return RpcValue.handle(marker.getId());
}
// 如果是 Map 初始数据 → 发 MAP,让手机 new TreeMap
if (value instanceof Map) {
    return RpcValue.map(copy);
}
// VarArg:反射读内部 args 列表,逐个翻译
public static DvmObject<?> wrapRemoteHandle(BaseVM vm, int handleId, String signature) {
    // 从 signature 看出返回值类型,比如 Iterator.next → Map$Entry
    String className = inferClassName(signature);
    DvmClass type = vm.resolveClass(className);
    // 造一个「壳」,里面只存远程编号,真对象在手机
    return new RemoteHandleDvmObject(type, new RemoteHandleMarker(handleId));
}
// Service 里启动,bind 127.0.0.1:8765
String line = reader.readLine();
RpcRequest req = 解析JSON(line);
RpcResponse resp = dispatcher.dispatch(req);  // 按 op + signature 调真 TreeMap/Iterator
writer.write(编码JSON(resp));
writer.newLine();
writer.flush();
writer.write(JSON字符串);
writer.newLine();
writer.flush();
String line = reader.readLine();
vm.setJni(this);   // 框架里自动包代理 + Hybrid
// java/* 不用 override 了
// android/* 包名、getPackageManager 仍在 PC 补

[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 10小时前 被r8e8cd8编辑 ,原因:
收藏
免费 15
打赏
分享
最新回复 (9)
雪    币: 375
活跃值: (3902)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
看了学习一下
11小时前
0
雪    币: 387
活跃值: (1856)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
666
8小时前
0
雪    币: 104
活跃值: (8672)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
tql
8小时前
0
雪    币: 76
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
别玩unidbg了,直接真机trace一遍完事了
4小时前
0
雪    币: 3897
活跃值: (9018)
能力值: ( LV7,RANK:102 )
在线值:
发帖
回帖
粉丝
6
为啥一定要在电脑上跑
4小时前
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
感谢分享
4小时前
0
雪    币: 764
活跃值: (3562)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
6666
2小时前
0
雪    币: 2815
活跃值: (5426)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
666
1小时前
0
雪    币: 2005
活跃值: (2387)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
plus
1小时前
0
游客
登录 | 注册 方可回帖
返回