Unidbg学习笔记(四):一条SVC指令引发的连锁反应

admin 2026-06-08 04:23:19 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档深入解析了Unidbg模拟器如何利用ARM架构的SVC指令统一处理JNI调用、系统调用和Hook拦截。通过将外部交互伪装成SVC中断,Unidbg实现了高效的分发机制,详细展示了从SO代码触发到Javahandler执行的完整链路。文章揭示了SVC指令在模拟器中的核心作用,为理解Unidbg底层原理提供了关键洞察。 综合评分: 85 文章分类: 逆向分析,二进制安全,红队,内网渗透,安全工具


cover_image

Unidbg学习笔记(四):一条 SVC 指令引发的连锁反应

原创

泡泡以安 泡泡以安

泡泡以安

2026年4月14日 09:09 浙江

在小说阅读器读本章

去阅读

理解 SVC 机制就理解了 Unidbg 的灵魂。所有的 JNI 调用、系统调用、Hook 拦截,最终都汇聚到同一个入口。


从一行汇编开始

打开 IDA 看一段 ARM64 的反汇编,你迟早会撞见这样一行:

.text:0000000000401234    svc    #0

短短四个字节(0x010000D4),却是整个用户态程序与内核打交道的唯一通道。open 是它,read 是它,gettimeofday 是它,mmap 也是它。所有看起来天差地别的”系统调用”,到最底层全都收敛成同一条指令。

更妙的是,Unidbg 在这条指令上动了手脚 — 它不只用 SVC 来模拟系统调用,还把 JNI 调用、Hook 回调、虚拟模块的函数实现,统统伪装成了 SVC。一条 ARM 指令,扛起了整个 Unidbg 的对外交互。

理解这个机制,你会明白几件以前可能一直困惑的事:

  • 为什么 JNI 报错和 Syscall 报错的栈帧长得几乎一模一样?
  • 为什么 Unidbg 的 Hook 能在 Frida 完全够不到的层级生效?
  • 为什么 intno=2 这个数字会出现在几乎所有 Unidbg 的报错里?

这一篇,我们就拆开这条 SVC 指令,看看它在 Unidbg 内部点燃了一连串什么样的连锁反应。


SVC 指令:用户态通往内核的那道门

ARM 架构中的 SVC

SVC 全称 Supervisor Call(管理调用),在 ARMv7 中曾叫 SWI(Software Interrupt,软件中断),ARMv8 之后统一改叫 SVC。作用只有一个:让 CPU 从用户态主动切换到内核态

它的指令格式非常朴素:

ARM32:  svc #imm24    ; 24 位立即数(实际只用低 8 位)
ARM64:  svc #imm16    ; 16 位立即数

机器码示例:
ARM64:  D4 00 00 01    →    svc #0
ARM32:  EF 00 00 00    →    svc #0

立即数本身在大多数 Linux 系统上是没有意义的(按惯例填 0)。系统调用号是通过寄存器传递的:

| 架构 | 系统调用号寄存器 | 参数寄存器 | 返回值寄存器 | | — | — | — | — | | ARM32 | r7 | r0r6 | r0 | | ARM64 | x8 | x0x5 | x0 |

举个具体例子,调用 read(fd, buf, count) 在 ARM64 上的样子是这样:

mov  x0, #3            ; 第一个参数 fd = 3
mov  x1, x19           ; 第二个参数 buf 指针
mov  x2, #1024         ; 第三个参数 count = 1024
mov  x8, #63           ; 系统调用号 __NR_read = 63 (ARM64 用 x8 装载)
svc  #0                ; 触发软件中断 → CPU 从 EL0 进入 EL1
                       ; 内核完成 read 后返回, 结果写在 x0

真实 Android 中发生了什么

在真机上执行这条 svc #0 时,CPU 内部发生的事情大概是:

  1. CPU 把当前的执行状态(PC、寄存器、PSTATE)保存到内核栈
  2. 切换异常级别:EL0(用户态)→ EL1(内核态)
  3. 跳转到内核预先注册的异常向量表(VBAR_EL1 指向的位置)
  4. 内核根据 x8 寄存器查 syscall 表,找到对应的内核函数(如 sys_read
  5. 执行内核函数,结果写回 x0
  6. 执行 eret 指令,恢复寄存器,返回用户态的下一条指令

SVC 陷入与返回:用户态到内核态的切换

整条用户态代码并不知道(也不需要知道)内核里到底发生了什么。它只知道:执行 svc #0,返回时 x0 里有结果。SVC 是一道单向门 — 你把请求扔进去,等待门那边的人把答案推回来。

类比一下:SVC 就像银行柜台的取号机。你不需要知道柜员是谁、后台系统是什么、钱从哪个金库取出来。你按下按钮(执行 SVC),等叫号(CPU 恢复执行),结果就在你手上了。柜台后面的世界,对你是完全不透明的。


Unidbg 的天才设计:把所有外部交互都伪装成 SVC

一个棘手的工程问题

现在切换到 Unidbg 作者的视角。你正在写一个 ARM 模拟器,需要让里面跑的 SO 代码能调用到外部的 Java 世界(这是 JNI 的本质)。问题来了:SO 代码是真实的 ARM 机器码,它怎么”跳出”模拟器去调用宿主机上的 Java 函数?

直觉上的方案有几个:

方案 A:扫描 PLT,识别 JNI 调用

Unidbg 在加载 SO 时遍历 PLT 表(Procedure Linkage Table,过程链接表),把每个 JNI 函数的导入项替换成自定义的处理逻辑。理论可行,但实现起来非常脏 — 每加载一个 SO 都要做一次符号解析,而且难以处理动态查表((*env)->FindClass(env, "...") 这种通过函数指针的调用)。

方案 B:拦截特定函数地址

为每个 JNI 函数指定一个伪造的地址(比如 0xdeadbeef00),SO 调用到这个地址时,模拟器抛出”非法访问”异常,然后在异常处理器里识别并分发。能跑,但语义上是”用 Bug 触发功能”,很别扭。

方案 C:复用 SVC 机制

把 JNI 函数表中的每一项,指向一段预先生成的 ARM 代码。这段代码里只有一条 SVC 指令。当 SO 通过函数表调用 JNI 函数时,自然会执行到这条 SVC,触发模拟器的中断回调,然后由 Unidbg 在回调里完成实际工作。

Unidbg 的作者选择了方案 C。这是一个看起来朴素、实际却精妙到令人拍案的决策

为什么方案 C 这么妙

方案 C 的精妙之处在于,它复用了 ARM 架构本来就有的”用户态 → 特权态切换”语义

  • ARM CPU 设计 SVC 的本意,就是”用户代码主动暂停,把控制权交给上层管理者”
  • 在真机上,”上层管理者”是 Linux 内核
  • 在 Unidbg 中,”上层管理者”就是 Unidbg 自己

这个等价关系一旦建立,所有事情都顺了:

  1. SO 代码不需要任何修改 — 它认为自己在调用一个普通的函数指针,不知道背后是 SVC
  2. 模拟器的中断处理器只需要写一份 — 不管是 JNI 调用还是真正的 syscall,都走同一套分发流程
  3. 所有外部交互天然地被汇聚到一个入口 — 想加日志、Hook、监控?只在这一个地方加就够了

更精妙的是:Unidbg 不需要给每个 JNI 函数指定一个唯一的 SVC 立即数(如果用立即数区分,立即数空间很快会不够)。它的做法是:用 SVC 指令所在的内存地址本身作为唯一标识。因为每段 SVC 桩代码是动态分配在不同地址上的,PC 值就是天然的”函数 ID”。

JNI 桩代码长什么样

来看一个具体的例子。Unidbg 在初始化 DalvikVM 时,会为 200 多个 JNI 函数(FindClassGetMethodIDCallObjectMethodNewStringUTF…)每个都生成一段桩代码:

; FindClass 的 SVC 桩 (ARM64), 由 Arm64Svc.onRegister() 生成
; Unidbg 实际把这类桩分配在高地址区, 例如 0xfffe0030

0xfffe0030:  svc  #0          ; 触发中断, Unidbg 在回调里以 PC 为 key 查到 FindClass 的 Java handler
0xfffe0034:  ret              ; handler 执行完后, Unidbg 把结果写到 x0, 这里直接返回到调用者

JNIEnv 函数表(一个连续的指针数组,对应 JNINativeInterface 结构体)的对应槽位被填上 0xfffe0030。SO 代码里的 (*env)->FindClass(env, "java/lang/String") 经过 NDK 编译后大致是这样:

ldr  x8, [x0]              ; x0 = env, *env 是 JNINativeInterface 表的指针
ldr  x9, [x8, #0x30]       ; FindClass 在表中的偏移 (ARM64 下是 0x30)
mov  x0, x19               ; 第一个参数: env
mov  x1, x20               ; 第二个参数: 类名字符串地址
blr  x9                    ; 跳转 → 0xfffe0030 → svc #0 → 中断

最关键的一步是 blr x9:CPU 跳转到 0xfffe0030,下一条指令就是 svc&nbsp;#0。模拟器的中断回调被触发,Unidbg 检查当前 PC = 0xfffe0030,在自己维护的 Map<Address, Svc> 里查到这个地址对应 FindClass 的 handler,调用它,把结果写回 x0。然后 SO 代码继续从 blr 的下一行执行,它完全不知道刚才发生了什么

再打个类比:这就像你给朋友发微信,他在群里 @ 了一个机器人。机器人没有真的把消息转给后台 — 它直接在群里回复了你。从你的视角看,你只知道”我发了消息,得到了回复”,并不需要关心机器人是谁、后台在哪。Unidbg 就是那个机器人,SVC 指令就是 @ 它的方式。


一次完整的连锁反应:FindClass 走了哪些地方

把这个机制串起来看,威力才显出来。我们以一次真实的 FindClass("com/example/Util") 调用为例,跟着 CPU 走一遍完整的执行流。

SVC 触发的连锁反应:从 SO 代码到 Java handler

阶段 1:SO 代码触发调用

SO 中的 C 代码:

JNIEXPORT jstring JNICALL&nbsp;Java_com_example_Util_sign(
&nbsp; &nbsp; &nbsp; &nbsp; JNIEnv *env, jobject thiz, jstring input)&nbsp;{
&nbsp; &nbsp;&nbsp;// 第一步: 找到 com.example.Util 类
&nbsp; &nbsp; jclass cls = (*env)->FindClass(env,&nbsp;"com/example/Util");
&nbsp; &nbsp;&nbsp;// ... 后续逻辑
}

经过 NDK 编译,FindClass 这一行会变成几条 ARM64 指令:

; X19 保存了 env 指针
ldr &nbsp; x0, [x19] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; x0 = *env (指向 JNINativeInterface 表)
ldr &nbsp; x8, [x0,&nbsp;#0x30] &nbsp; &nbsp; &nbsp; &nbsp;; x8 = FindClass 函数指针 (表偏移 0x30)
mov &nbsp; x0, x19 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; arg0 = env
adr &nbsp; x1, aClassName &nbsp; &nbsp; &nbsp; &nbsp; ; arg1 = "com/example/Util" 字符串地址
blr &nbsp; x8 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 调用 → 跳转到 Unidbg 注册的 SVC 桩地址

阶段 2:跳转到 SVC 桩,触发中断

CPU 跳转到 SVC 桩地址(沿用前面的例子 0xfffe0030):

0xfffe0030: &nbsp;svc &nbsp;#0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 关键的一条: 把控制权交给 Backend
0xfffe0034: &nbsp;ret &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; handler 执行完后从这里返回到 SO 代码

执行 svc&nbsp;#0 时,Unicorn/Dynarmic 的指令模拟器识别出这是一条特权指令,立即触发预先注册的中断回调。在 Unidbg 中,这个回调由 AbstractEmulator 链路上的中断 handler 接收,最终调用到对应架构的 SyscallHandler

阶段 3:Unidbg 的中断 handler 开始分发

进入 Java 世界。Unidbg 的中断处理逻辑大致是这样的(伪代码,便于理解):

// 简化后的中断分发逻辑
public&nbsp;void&nbsp;hook(Backend backend,&nbsp;int&nbsp;intno, Object user)&nbsp;{
&nbsp; &nbsp;&nbsp;if&nbsp;(intno != ARMV7_EXCP_SWI && intno != ARMV8_EXCP_SVC) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 不是 SVC 触发的中断, 走其他路径 (如内存访问异常)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; }

&nbsp; &nbsp; Emulator<?> emulator = (Emulator<?>) user;
&nbsp; &nbsp; UnidbgPointer pc = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_PC);

&nbsp; &nbsp;&nbsp;// 关键: 用 PC 地址查 svcMemory 中注册的 handler
&nbsp; &nbsp; Svc svc = svcMemory.getSvc(pc.peer);
&nbsp; &nbsp;&nbsp;if&nbsp;(svc !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 命中 → 这是一个 JNI 调用 / 虚拟模块函数 / Hook 回调
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;long&nbsp;ret = svc.handle(emulator);
&nbsp; &nbsp; &nbsp; &nbsp; backend.reg_write(Arm64Const.UC_ARM64_REG_X0, ret);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// 没命中 → 这是一个真正的 Linux 系统调用
&nbsp; &nbsp;&nbsp;long&nbsp;NR = backend.reg_read(Arm64Const.UC_ARM64_REG_X8).longValue();
&nbsp; &nbsp; handleSyscall(emulator, NR);
}

注意这里的关键决策点:是用”PC 地址”还是”x8 寄存器”来分发,取决于这个 SVC 桩有没有被预先注册过。

  • 注册过的 → JNI / 虚拟模块 / Hook → 用地址查表
  • 没注册的 → 真正的 syscall → 用 x8 查 syscall 表

这就是 Unidbg 区分两类 SVC 的方式,没有冲突,一套机制

阶段 4:执行 FindClass 的 Java handler

svcMemory.getSvc(0xfffe0030) 命中,拿到的 Svc 对象是 Unidbg 内部对 FindClass 的包装。它的 handle 方法大致是:

// DalvikVM 中 FindClass 的注册片段 (简化)
Pointer _FindClass = svcMemory.registerSvc(new&nbsp;Arm64Svc("FindClass") {
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;long&nbsp;handle(Emulator<?> emulator)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; RegisterContext context = emulator.getContext();
&nbsp; &nbsp; &nbsp; &nbsp; Pointer env = context.getPointerArg(0); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// x0
&nbsp; &nbsp; &nbsp; &nbsp; Pointer namePtr = context.getPointerArg(1); &nbsp; &nbsp; &nbsp;// x1
&nbsp; &nbsp; &nbsp; &nbsp; String className = namePtr.getString(0); &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 读出 "com/example/Util"

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 转交给用户的 Jni 实现 (通常是 AbstractJni 子类)
&nbsp; &nbsp; &nbsp; &nbsp; DvmClass dvmClass = checkJni(vm,&nbsp;this).resolveClass(className);

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 把 DvmClass 注册到本地引用表, 返回它的 jobject 句柄
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;dvmClass.hashCode();
&nbsp; &nbsp; }
});

这一步进入了”补环境”的领地 — resolveClass 最终会调用到你写的 AbstractJni 子类的 resolveClass 方法。如果你重写了,返回你提供的 DvmClass;如果你没重写,Unidbg 抛出”resolveClass not implemented”的异常。

等等,为什么 JNI 报错的栈帧里总有一行 svc handle 现在你应该明白了。所有 JNI 调用的”现场”都是这一条 SVC 指令触发的中断回调。报错栈帧的最底部,永远是中断 handler;上面那一长串,才是你写的 Java 代码。

阶段 5:返回值写回,SO 代码继续执行

handle 方法返回一个 longDvmClass 的句柄)。Unidbg 把它写到 x0 寄存器,然后让模拟器从 SVC 桩的下一条指令(ret)继续执行。ret 指令把控制流返回到 SO 代码中 blr x8 的下一行,整个 FindClass 调用结束。

从 SO 代码的视角看,它就像调用了一个普通的函数:传入参数,得到返回值。中间发生的所有事情 — 中断、异常、查表、回调到 Java、用户的 AbstractJni 处理 — 对它来说是完全透明的。


同一道门的不同来客

SVC 这道门,进出的不只是 JNI 调用。让我们看看 Unidbg 中所有走 SVC 的路径,你会发现这个设计的统一之美。

五类 SVC 调用走的同一条分发链路

来客一:JNI 函数表

刚才详细讲过的。JNIEnv 和 JavaVM 函数表里的每一项都是一段 SVC 桩。约 200 多个函数,对应 200 多段桩代码。

关键文件unidbg-android/src/main/java/com/github/unidbg/linux/android/dvm/DalvikVM64.java(ARM64 版)和 DalvikVM.java(ARM32 版),里面密密麻麻的 svcMemory.registerSvc(new ArmSvc(...) {...}) 就是注册过程。

来客二:Linux 系统调用

SO 代码或 libc 内联汇编里的 svc&nbsp;#0。这一类是真的会跑到内核处理函数(Unidbg 的 ARM32SyscallHandler / Arm64SyscallHandler),需要根据 x8(或 r7)的系统调用号分发到具体的实现。

关键文件unidbg-android/src/main/java/com/github/unidbg/linux/ARM32SyscallHandler.java 和 Arm64SyscallHandler.java。打开看看你会被 case 语句的密度震撼到 — 一个超长的 switch,每个 case 是一个 syscall 编号。

来客三:虚拟模块的导出函数

Unidbg 支持”虚拟模块”(VirtualModule)— 比如 libc.so 中的某些函数(如 __system_property_get),Unidbg 自己用 Java 实现,然后注册成一个看起来像真 SO 的模块。每个被虚拟化的函数的”代码”,其实也是一段 SVC 桩。

当 SO 通过 PLT 跳转调用 __system_property_get 时,跳转的目标就是这段桩代码,于是 SVC 触发,Unidbg 接管。

来客四:HookZz / Whale 的回调

Inline Hook 的实现需要在被 Hook 的函数入口插入一段代码,让原本的执行流转跳到用户的回调函数。Unidbg 的实现里,这段”插入的代码”也是一段 SVC 桩。

也就是说:当你用 HookZz Hook 了某个函数,函数入口被改成了 svc&nbsp;#0; ret,SO 一执行到这里就触发 SVC,Unidbg 在回调里执行你的 Java 代码。

来客五:SystemPropertyHook

这是 Unidbg 内置的、专门拦截 __system_property_get 系统属性获取的机制。它的工作方式是 SVC Hook — 在 SVC 中断 handler 里加一道前置检查:如果当前 PC 落在 __system_property_get 的 SVC 桩上,就调用用户注册的 PropertyProvider 来生成返回值。

层级关系是:

SO 调用 __system_property_get
&nbsp; ↓
PLT 跳转到 Unidbg 的 SVC 桩
&nbsp; ↓
svc&nbsp;#0&nbsp;→ 中断回调
&nbsp; ↓
SyscallHandler 检查 PC, 命中 __system_property_get
&nbsp; ↓
SystemPropertyHook 介入, 调用 PropertyProvider
&nbsp; ↓
返回属性值, ret 回 SO 代码

这就回答了第十篇大纲里提到的一个常见错误:为什么 SystemPropertyHook 必须在 loadLibrary 之前注册? 因为 PLT 解析是在 loadLibrary 时完成的,PLT 表项指向的桩代码地址在那一刻就被定下来了,之后再注册 Hook,桩地址都没你的份。


这个设计对使用者意味着什么

1. 所有报错都长得很像

你在用 Unidbg 时一定见过这种报错栈:

java.lang.UnsupportedOperationException: callObjectMethod
&nbsp; com/example/Util->getDeviceId(Landroid/content/Context;)Ljava/lang/String;
&nbsp; &nbsp; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:...)
&nbsp; &nbsp; at com.github.unidbg.linux.android.dvm.DalvikVM64$XXX.handle(DalvikVM64.java:...)
&nbsp; &nbsp; at com.github.unidbg.linux.ARM64SyscallHandler.hook(ARM64SyscallHandler.java:...)
&nbsp; &nbsp; ...

注意最底下那一行 — ARM64SyscallHandler.hook所有 JNI 报错的栈底,都是 SyscallHandler 的中断回调。第一次看可能觉得”明明是 JNI 问题,关 syscall handler 什么事?”,现在你知道了:JNI 报错和 syscall 报错本质上是同一类报错,都是 SVC 中断里抛出来的

这给排错带来一个实用技巧:看到任何报错时,先看倒数第二帧(紧挨着 hook 的那一层),它会告诉你这次中断的”性质”是什么 — JNI 函数?syscall?虚拟模块?

2. Hook 能做到 Frida 做不到的事

Frida 是注入到目标进程内的 V8 脚本引擎。它能 Hook 的最底层是用户态的函数入口(PLT、函数符号、内联汇编位置),但它没法 Hook SVC 指令本身。一旦 SO 代码执行了 svc&nbsp;#0,控制权就交给内核了,Frida 看不到内核里发生了什么。

Unidbg 不一样。SVC 是它自己实现的,所有 SVC 中断都从它手里过。你可以:

  • 在 SVC 中断回调里加一行日志,记录所有系统调用 — 包括 SO 代码内联汇编里手写的、连 libc 包装函数都没经过的那些
  • 在中断回调里改 syscall 编号,把一个 syscall 变成另一个
  • 监控某段时间内的 SVC 频率,检测加壳代码的反调试循环

后面的章节会介绍”指令级 Trace”和这一层关系密切:Trace 实现的本质是在 Unicorn 的指令执行回调里加日志,而 SVC 机制让你能在更高的抽象层次(”这是一次外部交互”,而不是”这是一条普通指令”)去做监控。

3. 性能边界与 Backend 的关系

回想第三篇讲的 Backend 选型。SVC 机制能解释一件事:为什么 Dynarmic 不支持指令级 Hook,但仍然支持 SVC 中断?

答案是:SVC 是 ARM 架构定义的特权指令。无论用解释执行(Unicorn)还是 JIT 编译(Dynarmic),SVC 都必须触发宿主机这边的处理器接管 — 它是模拟器和外部世界的契约边界。Dynarmic JIT 编译时,会为 SVC 指令保留一个回调钩子,编译后的本机代码执行到 SVC 时还是会调用 Unidbg 的 Java handler。

这就是为什么 Dynarmic 上 JNI 调用照样能跑、syscall 照样能处理 — 走 SVC 的那部分能力是所有 Backend 共享的。Dynarmic 只是丢掉了”在每一条普通指令前后插入回调”的能力,并没有丢掉”在 SVC 触发时插入回调”的能力。

| 能力 | 实现方式 | 所有 Backend 都支持? | | — | — | — | | 普通指令 Hook | 翻译时插入 x86 回调代码 | 否 (仅 Unicorn/Unicorn2) | | SVC 中断 | ARM 架构定义的特权指令陷入 | | | Trace | 翻译时记录每条指令 | 否 (仅 Unicorn/Unicorn2) | | JNI 调用 | 走 SVC 中断 | | | 系统调用 | 走 SVC 中断 | |

理解了这个矩阵,你就能解释自己之前可能踩过的坑:在 Dynarmic 上跑某个原本在 Unicorn 上工作的脚本,如果用了 traceCode 会失效,但补环境的 JNI 回调依然正常 — 它们走的不是同一条路


总结:一个入口,五种来客

Unidbg 的 SVC 机制是一个典型的”用对了原语”的设计:

  • ARM 架构本来就有 SVC 这个”用户态 → 特权态”的语义槽
  • Unidbg 没有自己发明轮子,而是复用了这个槽 — 把所有需要”跳出模拟器”的场景都映射到 SVC
  • 一个统一的中断回调入口,承载了 JNI、syscall、虚拟模块、Hook、SystemProperty 五类完全不同的交互
  • 不同 Backend 在 SVC 处理上保持兼容,让”分析能力”和”执行效率”的取舍只发生在 SVC 之外的指令上

下次再看到 Unidbg 报错栈底的 ARM64SyscallHandler.hook,你应该不会再困惑了。那是一道门 — 一道用一条 ARM 指令撑起整个 Unidbg 调度系统的门。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:泡泡以安 泡泡以安 泡泡以安《Unidbg学习笔记(四):一条 SVC 指令引发的连锁反应》

评论:0   参与:  0