fuqiuluo’s blog

记录美好生活

日志记录

unicorn - 检测unicorn的几个方案

#安卓#逆向#安全#unicorn
type
Post
status
Published
date
Jul 10, 2026
slug
summary
tags
安卓
逆向
安全
unicorn
category
日志记录
icon
password

1. 执行层级与运行身份不一致

1.1. 简介

普通 Android native 代码运行在 EL0:
这两个寄存器都要求至少 EL1 权限。正常情况下:
  1. CPU 执行 MRS
  1. 产生未定义指令异常。
  1. Linux 检查是否属于允许向用户态模拟的系统寄存器。
  1. CurrentELSCTLR_EL1 不在允许的用户态 ID 寄存器范围中。
  1. 最终进程收到 SIGILL
Linux 确实会为部分 ID 寄存器模拟 MRS,例如 MIDR_EL1ID_AA64ISAR0_EL1,所以“某个 EL1 命名的寄存器可读”不能直接判定异常;但未开放的系统寄存器会导致 SIGILLLinux ARM64 CPU feature registers
因此要特别选择:
而不能仅仅选择 MIDR_EL1

1.2. 原理

本地我使用的是 Unicorn 2.1.4,对应提交: 8028ec436f2d9376525352dd38ed9ed6b9f6be10
Unicorn 创建 ARM64 CPU 时,如果没有主动设置 CPU model,会选择 A72:
cpu64.c
unicorn-engine
随后 Unicorn 又直接把 PSTATE 设置成:
cpu64.c
unicorn-engine
PSTATE_MODE_EL1h 的编码是:
计算当前 EL 时,Unicorn 提取 PSTATE 的 [3:2]
cpu.h
unicorn-engine

1.3. 检测代码

探测覆盖这几个点:
  • CurrentEL 指令居然成功返回
  • CurrentEL 表示 EL1/EL2/EL3
  • SCTLR_EL1 也能成功读取
实际探测结果:

2. CPU 静态元组指纹

核心: 一次读取多个描述 CPU 身份、指令集、内存系统和缓存结构的寄存器

2.1. Unicorn 2.1.4 的硬编码元组

模型
MIDR_EL1
ISAR0_EL1
MMFR0_EL1
CTR_EL0
A53
0x410fd034
0x00011120
0x00001122
0x84448004
A57
0x411fd070
0x00011120
0x00001124
0x8444c004
A72
0x410fd083
0x00011120
0x00001124
0x8444c004

2.2.MIDR_EL1 怎么解码

拆解结果:
所以单看 MIDR,它确实自称一颗 Cortex-A72。
问题是:真实 Cortex-A72 设备也可能返回这个 MIDR。因此 MIDR_EL1 == 0x410fd083 单独只能算弱证据。

3. CPU 与 Android/Linux 跨层一致性

Unicorn 可以模拟 ARM64 指令,也可以让程序读取一组看起来真实的 CPU 寄存器。但一个 Android 进程看到的运行环境不只有 CPU,还包括 Linux 内核提供的用户态能力信息。
因此,我们可以询问同一个问题两次:这台 CPU 支持哪些指令?
第一次询问 CPU 特征寄存器,第二次询问 Linux。若答案互相矛盾,说明 CPU 层和操作系统层很可能不是由同一套真实环境产生的。

3.1. CPU 给出的答案

ARM64 使用 ID_AA64ISAR0_EL1 描述部分指令集能力。Unicorn 的默认 Cortex-A72 模型将它固定为:
这个值表示 CPU 支持:
如果只看 CPU 层,这是一颗能力正常的 Cortex-A72。

3.2. Linux 给出的答案

Linux 不要求普通应用自己判断每颗 CPU。它会在启动过程中汇总系统里所有 CPU 的能力,然后生成一个适合用户态使用的能力位图,称为 HWCAP。
其中:
所以前面五种能力对应:
当 Linux 创建一个新进程时,会把 HWCAP 放进进程的 auxiliary vector,简称 auxv:
auxv 是 Linux 在 execve() 阶段交给新进程的一张启动信息表。页面大小、程序入口、随机数地址和 CPU 能力都保存在这里。
同一份 auxv 还会保存在进程的:
因此,读取:
得到的就是 Linux 创建当前进程时生成的启动信息。

3.3. 源码索引

base.c
torvalds

4. 独占监视行为异常

前面的检测主要观察寄存器值。来一点巧思, 不再检查静态数据,而是观察一段指令跨越系统调用之后,CPU 内部状态是否发生了正确变化。检测序列是:
它可以判断 SVC 是否真的经历了 ARM 异常进入和异常返回。

4.1. 独占监视器是什么

ARM64 使用 LDXR/STXR 实现原子操作。
执行 LDXR 时,CPU不仅从 [x0] 读取数据,还会在内部记录:
这个内部状态称为 Local Exclusive Monitor,局部独占监视器。随后执行 STXR
最简单的情况:
中间没有发生任何事情,通常得到:
如果主动清除独占状态:
则必然得到:
这组 CLREX 序列可以作为对照实验。

4.2. 系统调用为什么会影响独占状态

Android 应用运行在 EL0。执行:
会产生同步异常,CPU进入 EL1,Linux 处理系统调用,最后执行 ERET 返回 EL0。完整路径是:
ARM 异常返回会清除局部独占监视器。因此:
在真实 Android/Linux 上,STXR 应该失败:
Linux 返回用户态的最终指令确实是 ERET
entry.S
torvalds

4.3. Unicorn 哪里出了差异

nicorn 对 LDXR 的实现,会把独占地址保存到:
核心代码:
translate-a64.c
unicorn-engine
执行 STXR 时,Unicorn先比较当前地址和保存的独占地址:
成功返回 0,失败返回 1
问题出现在 SVC。Unicorn 将 SVC 转换为:
源码:SVC 翻译
随后调用使用者注册的:
典型 Python 模拟器会在 hook 里:
  1. 读取 x8,取得系统调用号。
  1. 计算返回值。
  1. 把返回值写入 x0
  1. SVC 后面继续执行。
Unicorn 的处理代码在 hook 返回后直接清除异常编号:
这里没有真正进入 EL1,没有运行 Linux 异常处理程序,也没有执行 ERET
因此独占状态可能仍然存在:

4.4. 为什么问题在异常返回?

Unicorn/QEMU 本身其实知道 ERET 应该清除独占状态。它实现异常返回时会调用:
helper-a64.c
unicorn-engine
清除操作本身就是:
internals.h
unicorn-engine
所以问题不是 Unicorn 不会处理独占监视器,而是简单的 syscall hook 绕过了完整的异常返回路径。

4.5. 检测代码

这里选择无副作用的 getpid
在 Unicorn 2.1.4 的 A53、A57、A72 和 max 模型中均得到:
 
Loading...