0%

Xposed AppSettings权限管理原理分析

本文分析一个权限管理类Xposed模块的源代码,主要分析权限管理功能实现的原理。完全按照本人看代码的顺序写成。写此文主要不是为了分析代码,而是总结这种分析代码的思路。所以,懒得看过程可以直接跳到代码分析

# 准备工作

下载好源代码,还要在手机上把这个程序安装好,这样能直观感受它的功能。

# 大致思路

我们最关键的任务是找到权限控制的核心代码并弄明白它的功能。但是这么多的文件无从下手。我的想法是结合程序的实际操作,然后翻出来相应的代码。

直奔主题

先看AndroidManifest.xml,因为我们要找程序启动界面。除了xposed的meta data,有个定义launcher activity的代码:

<activity
    android:name=".XposedModActivity"
    android:label="@string/app_name"
    android:configChanges="orientation|screenSize">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

就是说类de.robv.android.xposed.mods.appsettings.XposedModActivity包含启动界面的代码。

在手机上打开程序,会看到启动界面主要是一个ListView,每项里面放着app的名称和包名。点击其中某项会跳到新的Activity里。那么目标明确了,找这个跳转代码。

我用startAcitivity为关键词找到了:

list.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                // Open settings activity when clicking on an application
                String pkgName = ((TextView) view.findViewById(R.id.app_package)).getText().toString();
                Intent i = new Intent(getApplicationContext(), ApplicationSettings.class);
                i.putExtra("package", pkgName);
                startActivityForResult(i, position);
            }
});

可见,ApplicationSettings这个类包含了新的Activity的代码。

新界面只有一个switch,打开switch后所有的选项都出来了。我手机的左下角出现了一个叫”权限管理“的按钮。点开以后是一个对话框,里面的ListView罗列了所有的应用权限让我们修改。单击List里的某项,权限就被禁用了,同时权限的字体由白变紫。

所以我先找这个按钮的代码:

btnPermissions.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // set up permissions editor
                try {
                    final PermissionSettings permsDlg = new PermissionSettings(ApplicationSettings.this, pkgName, allowRevoking, disabledPermissions);
                        permsDlg.setOnOkListener(new PermissionSettings.OnDismissListener() {
                        @Override
                        public void onDismiss(PermissionSettings obj) {
                            allowRevoking = permsDlg.getRevokeActive();
                            disabledPermissions.clear();
                            disabledPermissions.addAll(permsDlg.getDisabledPermissions());
                        }
                    });
                    permsDlg.display();
                } catch (NameNotFoundException e) {
                }
            }
});

看来PermissionSettings就是权限管理界面的类。在这个类中唯一被我发现的和ListView有关的代码:

// Load the list of permissions for the package and present them
    loadPermissionsList(pkgName);

    final PermissionsListAdapter appListAdapter = new PermissionsListAdapter(owner, permsList, disabledPerms, true);
    appListAdapter.setCanEdit(revokeActive);
    ((ListView) dialog.findViewById(R.id.lstPermissions)).setAdapter(appListAdapter);
    

所以说处理单击ListView项并改变程序权限的代码应该在别的地方。

只能是在Adapter的代码里了:

if (allowEdits) {
    row.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (!canEdit) {
                return;
            }

            TextView tv = (TextView) v.findViewById(R.id.perm_name);
            if ((tv.getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0) {
                disabledPerms.remove(tv.getTag());
                tv.setPaintFlags(tv.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
                tv.setTextColor(Color.WHITE);
            } else {
                disabledPerms.add((String) tv.getTag());
                tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
                tv.setTextColor(Color.MAGENTA);
            }
        }
    });
}

在这里disabledPerms是一个Set<String>类型的对象。顾名思义,这里面方的可能是被禁用的权限。上面代码并没有直接处理权限的部分。结合Diaglog界面有“确定”按钮,推测最后程序先将权限添加到Set中,然后统一禁止权限。

那么disabledPerms怎么传递出去的呢?搜索整个Adapter的代码,看到构造函数里有一句:this.disabledPerms = disabledPerms;。再回头看该才找到的PermissionSettings类里和List唯一有关的代码里有:

final PermissionsListAdapter appListAdapter = new PermissionsListAdapter(owner, permsList, disabledPerms, true);

所以disabledPerms就是我们要找的。下一步可以找PermissionSettings里的这个disabledPerms里的结果怎么返回回去的。搜索这个类里的代码,找到了get方法:

/**
* Get the list of permissions in the disabled state
 */
public Set<String> getDisabledPermissions() {
    return new HashSet<String>(disabledPerms);
}

我们之前找到的ApplicationSettings里面的btnPermissions.setOnClickListener里有这么一行:disabledPermissions.addAll(permsDlg.getDisabledPermissions());

另外disabledPermissions只有声明没有定义。在onCreate()方法里它才被赋值:

// Setting for permissions revoking
allowRevoking = prefs.getBoolean(pkgName +   Common.PREF_REVOKEPERMS, false);
disabledPermissions = prefs.getStringSet(pkgName + Common.PREF_REVOKELIST, new HashSet<String>());

同时在private Map<String, Object> getSettings()这个方法结尾处有这么两行:

if (disabledPermissions.size() > 0)
    settings.put(pkgName + Common.PREF_REVOKELIST, new HashSet<String>(disabledPermissions));
    

方法的返回值就是settings。如果再看看整个方法的代码,可知这个应用的所有被修改内容全放到这个settings里了。

onOptionsItemSelected里出现了getSettings()的调用,同时还有很多sharedPreference的操作。在手机上,我们修改应用权限,然后单击右上角的保存按钮,弹出提示对话框,询问是否结束进程以便下次启动时采用新设置。根据这点找到代码:

prefsEditor.commit();

// Update saved settings to detect modifications later
initialSettings = newSettings;

// Check if in addition to saving the settings, the app should also be killed
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.settings_apply_title);
builder.setMessage(R.string.settings_apply_detail);
builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        // Send the broadcast requesting to kill the app
        Intent applyIntent = new Intent(Common.MY_PACKAGE_NAME + ".UPDATE_PERMISSIONS");
        applyIntent.putExtra("action", Common.ACTION_PERMISSIONS);
        applyIntent.putExtra("Package", pkgName);
        applyIntent.putExtra("Kill", true);
        sendBroadcast(applyIntent, Common.MY_PACKAGE_NAME + ".BROADCAST_PERMISSION");

        dialog.dismiss();
    }
});

整个工程只有一个PackagePermissions类是BroadcastReceiver类。打开后发现这个类有大量的Xposed的hook函数。那么问题来了:

  • 广播接受器什么时候开始工作的?
  • 哪些是hook权限的操作?
  • hook是如何在开机时就开始了(否则没办法监控权限)

在BroadcastReceiver里叫initHooks()的静态方法里找到:

final Class<?> clsPMS = findClass("com.android.server.pm.PackageManagerService", XposedMod.class.getClassLoader());

// Listen for broadcasts from the Settings part of the mod, so it's applied immediately
findAndHookMethod(clsPMS, "systemReady", new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        Context mContext = (Context) getObjectField(param.thisObject, "mContext");
        mContext.registerReceiver(new PackagePermissions(param.thisObject),
                new IntentFilter(Common.MY_PACKAGE_NAME + ".UPDATE_PERMISSIONS"),
                Common.MY_PACKAGE_NAME + ".BROADCAST_PERMISSION",
                null);
    }
});

// if the user has disabled certain permissions for an app, do as if the hadn't requested them
findAndHookMethod(clsPMS, "grantPermissionsLPw", "android.content.pm.PackageParser$Package", boolean.class,
        new XC_MethodHook() {
    @SuppressWarnings("unchecked")
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        String pkgName = (String) getObjectField(param.args[0], "packageName");
        if (!XposedMod.isActive(pkgName) || !XposedMod.prefs.getBoolean(pkgName + Common.PREF_REVOKEPERMS, false))
            return;

        Set<String> disabledPermissions = XposedMod.prefs.getStringSet(pkgName + Common.PREF_REVOKELIST, null);
        if (disabledPermissions == null || disabledPermissions.isEmpty())
            return;

        ArrayList<String> origRequestedPermissions = (ArrayList<String>) getObjectField(param.args[0], "requestedPermissions");
        param.setObjectExtra("orig_requested_permissions", origRequestedPermissions);

        ArrayList<String> newRequestedPermissions = new ArrayList<String>(origRequestedPermissions.size());
        for (String perm: origRequestedPermissions) {
            if (!disabledPermissions.contains(perm))
                newRequestedPermissions.add(perm);
            else
                // you requested those internet permissions? I didn't read that, sorry
                Log.w(Common.TAG, "Not granting permission " + perm
                        + " to package " + pkgName
                        + " because you think it should not have it");
        }

        setObjectField(param.args[0], "requestedPermissions", newRequestedPermissions);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        // restore requested permissions if they were modified
        ArrayList<String> origRequestedPermissions = (ArrayList<String>) param.getObjectExtra("orig_requested_permissions");
        if (origRequestedPermissions != null)
            setObjectField(param.args[0], "requestedPermissions", origRequestedPermissions);
    }

分析代码

BroadcastReceiver里的注释说:

/* Hook to the PackageManager service in order to
* - Listen for broadcasts to apply new settings and restart the app
* - Intercept the permission granting function to remove disabled permissions
*/

也就是说监听器hook到了Android系统的包管理器类com.android.server.pm.PackageManagerService,并且hook了里面的方法。所以上面提到的哪些是hook权限的操作基本上解决了,代码之后详细分析。那么广播什么时候开始接收的?在IntelliJ里搜索BroadcastReceiver被用到的地方,发现:

  1. XposedMod类中initZygote方法里出现PackagePermissions.initHooks();
  2. 刚才贴出的大段BroadcastReceiver里出现mContext.registerReceiver里面有它的构造函数。

initZygote是Xposed框架IXposedHookZygoteInit接口中要自己实现的方法。接口的源代码为:

/**
* Hook the initialization of Zygote (the central part of the "Android OS")
*/
public interface IXposedHookZygoteInit extends IXposedMod {
/**
 * Called very early during startup of Zygote
 * @throws Throwable everything is caught, but will prevent further initialization of the module
 */
    public void initZygote(StartupParam startupParam) throws Throwable;

    public static class StartupParam {
        public String modulePath;
 }
}

这就意味着每当启动一个进程,都会执行initZygote里监听器的initHooks()方法来给包管理器挂钩。权限监听的钩子应该挂到com.android.server.pm.PackageManagerService这个类的名为的grantPermissionsLPw方法上。程序用了Xposed框架提供的findAndHookMethod方法。通过这个方法接收的参数,我们得知被hook的grantPermissionsLPw方法接收两个参数,分别是Package类型(android.content.pm.PackageParser的内部类),和boolean。initHooks()方法还顺带注册监听器来接受来自appSettings这个app自己发出的广播。再进一步查看工程代码和安卓源代码,就知道,appSettings的权限拦截原理是这样的:

原来的权限授权以前先:从之前appsSettings存储的sharedPreferences里取得对应应用的权限列表,放到Set<String> disabledPermissions里。并用ArrayList<String> origRequestedPermissions存放应用索要的权限,并利用Xposed自带的方法存储一份这个权限列表到param对象里。然后通过for each循环,对比两个ArrayList,生成第三张表newRequestedPermissions,并用setObjectField方法替换掉了原来Package对象的requestedPermissions对象。

之后,安卓系统会按照被我们“调包”的权限清单执行程序。

被hook过的方法执行以后:从param对象取出我们刚刚保存的原始的权限列表,然后再次用setObjectField把这个原始列表复原回去。

于是第二个问题,哪些是hook操作,怎么hook的问题就解决了。

最后,我们打开工程目录下assets/xposed_init文件,看到de.robv.android.xposed.mods.appsettings.XposedMod。所以,Xposed框架开始执行的就是这个类里的代码。它实现了IXposedHookZygoteInitIXposedHookLoadPackage接口。所以能:1.在zygote启动时执行,从而管理权限。2.在应用app的包加载前执行hook操作,替换应用资源(这是appSettings另一个功能,但本文不分析)。

所以,为何启动时就能hook的问题也解决了。