绕过激活 device owner 时的账号限制
Android 上,正常情况下,激活 device owner 时,设备上不能有账号,不过并非完全不能有,大部分情况下,把持有那个账号的对应 app 禁用了就可以解决。
前段时间,有人给我的项目 OwnDroid 开了一个 issue (#371) ,说有办法绕过激活 device owner 时的账号限制。
他给了一个 XDA forums 的链接。打开链接,附件里有一个 shell 脚本和一个 jar 文件,jar 文件后面再说,先看它的脚本(仅截取了关键部分):
HELPER="${1:-}" # jar 文件的路径,通过命令行参数传入
COMPONENT="com.rosan.dhizuku/.server.DhizukuDAReceiver" # Dhizuku 的 DeviceAdminReceiver
BRIDGE=com.shift.research.SetupStateBridge # jar 里的类名
refresh_setup_cache() { # 执行 jar 里的代码
CLASSPATH="$HELPER" app_process /system/bin "$BRIDGE" force-update 0
}
# 关键代码
settings put secure user_setup_complete 0
refresh_setup_cache
dpm set-device-owner "$COMPONENT"
settings put secure user_setup_complete 1
refresh_setup_cache
我可以把这个 XDA forums 的链接直接放在项目的 README.md 里,但对于用户来说,这十分不友好,大部分 OwnDroid 用户应该是用 Shizuku 激活 OwnDroid 的,而他们启动 Shizuku 的方式应该是无线调试,咱不能指望他们有电脑、会下载安装 adb 、会执行 shell 命令。
修改设置
既然大部分用户通过 Shizuku 或 root 激活 OwnDroid ,那我们在 OwnDroid 里自己实现修改设置就行了。
最简单的方式就是,在 Shizuku 的 user service 里,直接用Runtime.getRuntime().exec()执行那个脚本。
它好像只能执行单行命令,不支持多行命令和 bash 语法,但最多也就是包一层sh -c xxx.sh的事。
但这么做不优雅。Shizuku 设计出来就是为了让开发者直接在 Java/Kotlin 代码中使用 Binder 和 system_server 通信,而不是 shell 命令。
(这部分是踩坑记录,最后还是用了 shell 命令,如果不感兴趣,可以直接跳到更新状态部分)
Settings
Android SDK 提供了Settings API ,先试试它。
用反射获取 context :
fun getContextUsingReflection(): Context {
return Class.forName("android.app.ActivityThread")
.getMethod("currentApplication").invoke(null) as Context
}
读取设置:
val context = getContextUsingReflection()
Settings.Secure.getString(context.contentResolver, "user_setup_complete")
报错如下:
java.lang.SecurityException: Given calling package android does not match caller's uid 2000
照着网上的说法,修改代码:
val ci = Binder.clearCallingIdentity()
Settings.Secure.getString(context.contentResolver, "user_setup_complete")
Binder.restoreCallingIdentity(ci)
没用,仍然报错。
看看 Shizuku-API 的 example 吧。找到 UserService.java :
public class UserService extends IUserService.Stub {
public UserService(Context context) {
Log.i("UserService", "constructor with Context: context=" + context.toString());
}
}
发现它有一个带 context 的 constructor ,那试试 Shizuku 给的 context :
class ShizukuService(): IUserService.Stub() {
lateinit var context: Context
constructor(ctx: Context) : this() {
context = ctx
}
}
也没用,获取设置时仍然会报错。
settings 命令
adb shell 里面也没有正常 context ,那settings命令是怎么读取和修改设置的呢?
我在 Android 源代码里找到了 SettingsService.java 。
final public class SettingsService extends Binder {
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
(new MyShellCommand(mProvider, false)).exec(
this, in, out, err, args, callback, resultReceiver);
}
// ...
}
我本以为是settings命令能通过某种方式获取到正常的Context,然后获取到ContentResolver,再通过Settings API 修改设置,没想到,它直接把命令行参数转发给 system_server ,在服务端做参数解析,在服务端修改设置。
这本质上还是 shell 命令啊,只不过是不用创建进程,并没有优雅多少,而且,Android 6 甚至没有这个 service 。
Context
回到开始。
ContentResolver是怎么知道包名和 UID 不匹配的呢?应该是Context的问题。那我想办法获取com.android.shell的 context 应该就行了。
val shellContext = getContextUsingReflection()
.createPackageContext("com.android.shell", 0)
结果还是同样的报错。
看看 context 里的 package name :
Log.d(TAG, "package name: ${shellContext.packageName}")
Log.d(TAG, "op package name: ${shellContext.opPackageName}")
前者是com.android.shell,但后者仍然是android,问题应该出在这里。
我去看了 [ContextImpl.java] ,虽然我可以手动构建一个opPackageName为com.android.shell的 context ,但这么做要用到一堆反射,更不优雅,还不如直接用Runtime.getRuntime().exec()。
看回 stacktrace
java.lang.SecurityException: Given calling package android does not match caller's uid 2000
...
at android.app.IActivityManager$Stub$Proxy.getContentProvider(IActivityManager.java:6397)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:8191)
at android.app.ContextImpl$ApplicationContentResolver.acquireProvider(ContextImpl.java:3847)
at android.content.ContentResolver.acquireProvider(ContentResolver.java:2528)
at android.provider.Settings$ContentProviderHolder.getProvider(Settings.java:3449)
at android.provider.Settings$NameValueCache.getStringForUser(Settings.java:3675)
at android.provider.Settings$Secure.getStringForUser(Settings.java:7219)
at android.provider.Settings$Secure.getString(Settings.java:7185)
...
找到 ActivityThread.java :
public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {
@UnsupportedAppUsage
public IContentProvider acquireProvider(Context c, String auth, int userId, boolean stable) {
// ...
ContentProviderHolder holder;
holder = ActivityManager.getService().getContentProvider(
getApplicationThread(), c.getOpPackageName(), auth, userId, stable);
// ...
}
}
再看回ContextImpl的getOpPackageName():
class ContextImpl extends Context {
private @NonNull AttributionSource mAttributionSource;
@Override
public String getOpPackageName() {
return mAttributionSource.getPackageName();
}
}
如果硬要去改opPackageName,那就得改mAttributionSource,不仅要用到一堆反射,而且在不同 Android 版本上还有可能不一样,不优雅。
那构造一个ContextImpl呢?
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
String opPackageName) {
// ...
ContextImpl context = new ContextImpl(null, mainThread, packageInfo,
ContextParams.EMPTY, null, null, null, null, null, 0, null, opPackageName,
DEVICE_ID_DEFAULT, false);
// ...
return context;
}
ActivityThread很好拿,反射就行,LoadedApk也不难,可以从现有的context里面获取。但,我觉得这还是太复杂了。
ContentProvider
在 Android 6 那时候,还没有SettingsService,那 shell 里的settings命令是怎么获取设置的?
找到 SettingsCmd.java 。
public final class SettingsCmd {
public void run() {
// ...
IActivityManager activityManager = ActivityManagerNative.getDefault();
IContentProvider provider = null;
IBinder token = new Binder();
ContentProviderHolder holder = activityManager.getContentProviderExternal(
"settings", UserHandle.USER_OWNER, token);
provider = holder.provider;
// ...
}
String getForUser(IContentProvider provider, int userHandle,
final String table, final String key) {
final String callGetCommand; // 例如 Settings.CALL_METHOD_GET_SYSTEM
// ...
Bundle arg = new Bundle();
arg.putInt(Settings.CALL_METHOD_USER_KEY, userHandle);
Bundle b = provider.call(resolveCallingPackage(), callGetCommand, key, arg);
if (b != null) {
result = b.getPairValue();
}
}
public static String resolveCallingPackage() {
switch (android.os.Process.myUid()) {
case Process.ROOT_UID: {
return "root";
}
case Process.SHELL_UID: {
return "com.android.shell";
}
default: {
return null;
}
}
}
}
它是直接通过ContentProvider查询设置的,我也试试。
ActivityManagerNative在新的安卓上已经弃用,用ServiceManager.getService()就行。
IActivityManager需要反射,不方便,看看Settings是怎么获取ContentProvider的:
String AUTHORITY = "settings";
Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/global");
ContentProviderHolder mProviderHolder = new ContentProviderHolder(CONTENT_URI);
IContentProvider cp = mProviderHolder.getProvider(contentResolver);
绕了一圈,还是要用到ContentResolver吗?我本以为像 SettingsCmd.java 那样,用ContentProviderHolder.provider就可以,但,Settings里的ContentProviderHolder是Settings.ContentProviderHolder,并非 SettingsCmd.java 用到的android.app.ContentProviderHolder。
IActivityManager.getContentProviderExternal()在不同安卓版本上有不同的参数,麻烦。
Shell 命令
现在,shell 命令反倒是成了最优雅的做法。在 Shizuku 的 UserService 里:
Runtime.getRuntime().exec("settings put secure user_setup_complete 0")
更新状态
用 JADX 反编译那个 jar 文件。
(已简化代码)
package com.shift.research;
public final class SetupStateBridge {
/* 命令行参数为 force-update 和 0 */
public static void main(String[] strArr) throws Exception {
Object devicePolicyService = getDevicePolicyService();
Method findForceUpdateMethod = Class.forName("android.app.admin.IDevicePolicyManager")
.getMethod("forceUpdateUserSetupComplete", Integer.TYPE);
int parseInt = Integer.parseInt(strArr[1]);
invoke(findForceUpdateMethod, devicePolicyService, Integer.valueOf(parseInt));
}
private static Object getDevicePolicyService() throws Exception {
Class<?> cls = Class.forName("android.os.ServiceManager");
Class<?> cls2 = Class.forName("android.os.IBinder");
// 通过 ServiceManager 获取 DevicePolicyManagerService 的 Binder
Object invoke = cls.getMethod("getService", String.class).invoke(null, "device_policy");
Object invoke2 = Class.forName("android.app.admin.IDevicePolicyManager$Stub")
.getMethod("asInterface", cls2).invoke(null, invoke);
return invoke2;
}
private static void invoke(Method method, Object obj, Object... objArr) throws Exception {
method.invoke(obj, objArr);
}
}
简单来说,就是调用了 DevicePolicyManagerService 的forceUpdateUserSetupComplete()来更新它内部的状态。
最终代码
(已简化)
@Keep
class ActivationService : IActivationService.Stub() {
override fun execute(command: String): Bundle? {
val bundle = Bundle()
val process = Runtime.getRuntime().exec(command)
// 此处省略把 stdout 和 stderr 放到 Bundle 里的代码
return bundle
}
override fun activateDeviceOwner(): Bundle? {
var bypassed = false
// 那个 shell 脚本会在修改设置前检查 user_setup_complete ,只不过上面我把它省略了
val getProc = Runtime.getRuntime().exec("settings get secure user_setup_complete")
val getOutput = getProc.inputStream.readBytes().decodeToString()
if (getOutput == "1" || getOutput == "1\n") {
val putProc = Runtime.getRuntime().exec("settings put secure user_setup_complete 0")
val exitCode = putProc.waitFor()
Log.d(TAG, "Setting user_setup_complete, exit code $exitCode")
bypassed = exitCode == 0
} else {
// 未知情况,不尝试绕过设置
Log.w(TAG, "Get user_setup_complete, expected 1, actual $getOutput")
}
if (bypassed) forceUpdateSetupComplete()
val setOwnerResult = execute("dpm set-device-owner com.bintianqi.owndroid/.Receiver")
if (bypassed) { // 还原设置
val putProc = Runtime.getRuntime().exec("settings put secure user_setup_complete 1")
putProc.waitFor()
forceUpdateSetupComplete()
}
return setOwnerResult
}
private fun forceUpdateSetupComplete() {
// 更新 DevicePolicyManagerService 内部状态
try {
val binder = ServiceManager.getService(Context.DEVICE_POLICY_SERVICE)
val dpm = IDevicePolicyManager.Stub.asInterface(binder)
if (Build.VERSION.SDK_INT >= 31) {
dpm.forceUpdateUserSetupComplete(0)
} else {
dpm.forceUpdateUserSetupComplete()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to force update DPMS state", e)
}
}
companion object {
const val TAG = "ActivationService"
}
}
ServiceManager和IDevicePolicyManager都是隐藏 API ,我自己定义了 interface 。
package android.app.admin;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import androidx.annotation.Keep;
@Keep
public interface IDevicePolicyManager extends IInterface {
@Keep
abstract class Stub extends Binder implements IDevicePolicyManager {
public static IDevicePolicyManager asInterface(IBinder obj) {
throw new UnsupportedOperationException();
}
}
void forceUpdateUserSetupComplete();
void forceUpdateUserSetupComplete(int userId); // Android 12 及以上
}
package android.os;
import androidx.annotation.Keep;
@Keep
public interface ServiceManager {
static IBinder getService(String name) {
throw new UnsupportedOperationException();
}
}
测试,能在有账号的情况下激活 device owner。
顺手给 Dhizuku 开一个 issue (#347),把这个方法告诉它的开发者。
完成!