使用 Frida 运行时检测 Android 应用的真实权限状态 (App Ops)
使用 Frida 运行时检测 Android 应用的真实权限状态 (App Ops)
在 Android 安全分析和应用逆向工程中,了解一个应用拥有哪些权限至关重要。我们通常会首先查看 AndroidManifest.xml
文件来确定应用请求了哪些权限。然而,这仅仅是“静态”的声明。在现代 Android 系统中,用户可以在运行时授予、拒绝或限制权限,这使得静态分析的结果并不可靠。
应用可能请求了录音权限,但用户可能已经通过系统设置禁用了它。在这种情况下,应用虽然“声明”了该权限,但在运行时调用相关 API 时会失败。如何才能准确地知道应用在运行时某一时刻,是否真的被允许执行某个敏感操作呢?
答案是使用 Frida 结合 Android 的 AppOpsManager。本文将介绍 AppOpsManager
机制,并详细解析一个 Frida 脚本,该脚本可以实时检测目标应用是否真正获得了如录-音、访问位置等敏感操作的许可。
什么是 App Ops (App Operations)?
AppOpsManager
是 Android 4.3 (API 18) 引入的一套精细化的权限管理框架。它在标准的权限模型之上,提供了一个更细粒度的控制层。即使用户授予了某个权限(例如 android.permission.RECORD_AUDIO
),系统或用户仍然可以通过 App Ops 来禁止或“忽略”这个操作。
简单来说:
- 标准权限 (Standard Permissions):在
AndroidManifest.xml
中声明,用户在安装或运行时通过弹窗授予。决定了应用有没有资格执行某个操作。 - 应用操作 (App Ops):在系统层面进行跟踪和控制。决定了应用在执行这个操作时是否被真正允许。
很多国产手机的“权限管家”功能,比如返回空数据、拒绝后不再提醒等,底层就是基于 App Ops 机制实现的。因此,通过监控 App Ops,我们可以得到比标准权限检查更真实的结果。
Frida 脚本解析
我们的目标是编写一个 Frida 脚本,在附加到目标应用进程后,立即检查一系列我们关心的敏感操作权限的当前状态。
完整的 Frida 脚本 (check_op.js
)
var Context;
var AppOpsManager;
var AndroidProcess;
var ActivityThread;
var context;
var app;
var appOps;var op_check_list;function use_classes() {AppOpsManager = Java.use("android.app.AppOpsManager");Context = Java.use("android.content.Context");AndroidProcess = Java.use("android.os.Process");ActivityThread = Java.use("android.app.ActivityThread");app = ActivityThread.currentApplication();context = app.getApplicationContext();appOps = context.getSystemService(Context.APP_OPS_SERVICE.value);appOps = Java.cast(appOps, AppOpsManager);op_check_list = [AppOpsManager.OPSTR_RECORD_AUDIO.value, // 录音AppOpsManager.OPSTR_QUERY_ALL_PACKAGES.value, // 查询所有包AppOpsManager.OPSTR_ACCESS_MEDIA_LOCATION.value, // 访问媒体文件位置// 你可以在这里添加更多关心的权限// AppOpsManager.OPSTR_COARSE_LOCATION.value, // 访问粗略位置// AppOpsManager.OPSTR_FINE_LOCATION.value, // 访问精确位置];}function checkOp() {for (let opstr of op_check_list){var mode = appOps.checkOp(opstr,AndroidProcess.myUid(),context.getPackageName());console.log("OPSTR: " + opstr + "\tmode = " + mode + " (" + mode_to_string(mode) + ")");}
};// 辅助函数,用于将 mode 转换为可读字符串
function mode_to_string(mode) {if (mode == AppOpsManager.MODE_ALLOWED) {return "ALLOWED";}if (mode == AppOpsManager.MODE_IGNORED) {return "IGNORED/DENIED";}if (mode == AppOpsManager.MODE_ERRORED) {return "ERRORED";}if (mode == AppOpsManager.MODE_DEFAULT) {return "DEFAULT";}return "UNKNOWN";
}Java.perform(() => {console.log("--- Starting AppOps Check ---");use_classes();checkOp();console.log("--- AppOps Check Finished ---");}
)
脚本逻辑分解
-
use_classes()
函数:初始化与准备Java.use(...)
: 这是 Frida 的核心功能,用于获取 Java 类的引用,以便我们可以在 JavaScript 中调用它们的静态方法或实例化它们。ActivityThread.currentApplication()
: 一个非常实用的技巧,用于获取当前应用的Application
对象。app.getApplicationContext()
: 通过Application
对象获取全局的Context
(上下文)。Context
是访问 Android 系统服务的入口。context.getSystemService(...)
: 通过Context
获取AppOpsManager
系统服务。Java.cast(...)
: 因为getSystemService
返回的是一个通用的Object
,我们需要使用Java.cast
将它转换为具体的AppOpsManager
类型,这样 Frida 才能识别它拥有的方法(如checkOp
)。op_check_list
: 我们定义一个数组,存放所有我们想要检查的权限操作字符串。这些字符串都定义在AppOpsManager
的常量中,例如OPSTR_RECORD_AUDIO
对应的值是 “android:record_audio”。
-
checkOp()
函数:核心检测逻辑- 该函数遍历
op_check_list
中的每一个权限字符串。 - 关键调用是
appOps.checkOp(opstr, uid, packageName)
方法。- 第一个参数
opstr
: 要检查的操作字符串,如"android:record_audio"
。 - 第二个参数
uid
: 操作发起方的 User ID。我们使用android.os.Process.myUid()
来获取当前进程的 UID。 - 第三个参数
packageName
: 操作发起方的包名。我们使用context.getPackageName()
来获取当前应用的包名。
- 第一个参数
- 该方法会返回一个整数
mode
,这个mode
值代表了该操作的真实权限状态。
- 该函数遍历
-
mode
值的含义
checkOp
的返回值是理解脚本输出的关键。它主要有以下几种:AppOpsManager.MODE_ALLOWED
(值为 0): 允许。应用可以执行此操作。AppOpsManager.MODE_IGNORED
(值为 1): 忽略/拒绝。应用不允许执行此操作,并且调用相关 API 时系统会静默失败或返回空数据,不会导致应用崩溃。这是最常见的“拒绝”状态。AppOpsManager.MODE_ERRORED
(值为 2): 错误。应用不允许执行此操作,并且调用会直接抛出SecurityException
异常。AppOpsManager.MODE_DEFAULT
(值为 3): 默认。系统将根据默认规则来决定是否允许。通常会最终解析为ALLOWED
或IGNORED
。
为了方便阅读,我在脚本中增加了一个
mode_to_string
的辅助函数,可以将这些整数值转换成可读的字符串。
如何使用该脚本
使用这个脚本非常简单,只需要两个文件和一个正在运行的目标应用。
1. 附加脚本 (run.sh
)
为了方便,我们可以创建一个简单的 shell 脚本来启动 Frida 并附加到目标进程。
#!/bin/bash# 检查是否提供了参数
if [ -z "$1" ]; thenecho "用法: $0 <应用的包名或PID>"echo "例如: $0 com.example.app"echo "或: $0 12345"exit 1
fi# 附加到进程并加载JS脚本
# -U: 连接到USB设备
# -f: 启动并附加到指定的包名 (如果提供的是包名)
# -p: 附加到指定的PID (如果提供的是数字)
# -l: 加载脚本文件# 判断输入是包名还是PID
if [[ $1 =~ ^[0-9]+$ ]]; thenecho "正在附加到 PID: $1..."frida -U -p $1 -l check_op.js
elseecho "正在启动并附加到包名: $1..."frida -U -f $1 -l check_op.js --no-pause
fi
- 将上面的 Frida 代码保存为
check_op.js
。 - 将上面的 shell 脚本保存为
run.sh
。 - 赋予执行权限:
chmod +x run.sh
。
2. 执行检测
现在,假设你想检测 com.google.android.apps.messaging
这个应用的权限:
- 在你的电脑上打开一个终端。
- 运行脚本:
./run.sh com.google.android.apps.messaging
。
Frida 会自动启动该应用(如果尚未运行),注入脚本,然后你将看到类似下面的输出:
--- Starting AppOps Check ---
OPSTR: android:record_audio mode = 1 (IGNORED/DENIED)
OPSTR: android:query_all_packages mode = 0 (ALLOWED)
OPSTR: android:access_media_location mode = 1 (IGNORED/DENIED)
--- AppOps Check Finished ---
这个输出清晰地告诉我们:
- 该应用在当前运行时不被允许录音。
- 它被允许查询设备上安装的所有应用包。
- 它不被允许访问媒体文件的地理位置信息。
拓展与思考
这个脚本是一个“一次性”的快照,它展示了脚本运行时那一刻的权限状态。我们可以基于此进行更多有趣的探索:
- 持续监控:通过 Hook (钩子) 关键的 API (例如
android.media.MediaRecorder.start
),在 API 被调用前执行checkOp
,可以动态地观察权限状态的变化。 - 绕过检测:在更高级的攻防场景中,可以 Hook
AppOpsManager.checkOp
方法本身,修改其返回值,从而欺骗应用让它以为自己拥有某项权限。 - 权限审计:将
op_check_list
扩展到包含所有敏感的OPSTR_
常量,可以对一个应用进行全面的运行时权限审计,检查它是否有多余的、未被使用的授权。
总结
通过 Frida 与 AppOpsManager
的结合,我们能够穿透 Android 标准权限模型的表象,洞悉应用在运行时的真实行为许可。这不仅为安全研究人员和逆向工程师提供了一个强大的分析工具,也让我们对 Android 权限系统的复杂性和精妙性有了更深的理解。下次当你分析一个行为可疑的应用时,不妨用这个脚本来验证一下它的“真实面目”。