Android 11 和 MIUI 获取安装来源

近期分析一个恶意软件,需要知道是谁安装的它,本文记录 Android 11 和 MIUI 中获取安装者的方式。

背景知识

Android 10之前,开发者通常使用PackageManager.getInstallerPackage获取安装者的包名,真正调用后,会发现这个API基本毫无用处,只会返回 null、market(应用市场)、vendoring(GooglePlay)、installer(系统安装器。平时我使用浏览器、使用taptap安装游戏,都没有被正确地划分进来,因此这个 API 并没有按照预期工作。

Android 11后,多了一个 API 叫 public InstallSourceInfo getInstallSourceInfo(@NonNull String packageName),它是有意义的。

它返回 3 个 String,编写 APP 测试,会发现 getOriginatingPackageName 返回是空,另外两个字段也没什么价值。

  • getInitiatingPackageName
  • getOriginatingPackageName
  • getInstallingPackageName

我的 MIUI 手机,在应用设置界面可以看到 app 的安装信息,显然和上面的 API 不是同一个。

因此,引出本文的第一部分,MIUI设置界面的数据是哪里来的。

MIUI设置界面的数据哪来的

一眼定帧,鉴定为 com.miui.securitycenter/com.miui.appmanager.AMAppInformationActivity,直接把包拖出来分析。




最后发现是 com.miui.securitycenter.provider.SecurityCenterProvider 提供了数据。

这个 ContentProvider 有权限保护,shell权限无法访问,需要 root 后进行测试。

1
2
3
4
5
gauguinpro:/ $ su
gauguinpro:/ # content call --uri content://com.miui.securitycenter.provider --method openInstallerFile
xxxx
{"pkg_name":"org.cocos2d.Dice","installer_pkg_name":"com.taptap","update_pkg_name":"com.taptap"}
xxxxx

第一部分搞定,确实是存在数据库里的,而且看起来是MIUI专属的数据库,和AOSP无关。

于是提出下一个问题,在没有 root 的情况下,能否获取每个包的实际安装者。

AOSP的数据是哪来的

虽然 pm list packages -i 是错的,但也先看看它的实现吧。

文件位于:platform/frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java。它调用了 getInstallerPackageName ,那当然是错的。

1
2
3
4
5
6
7
8
9
10
11
/******/
final IPackageManager mInterface;
/******/
case "-i":
listInstaller = true;
/******/
if (listInstaller) {
pw.print(" installer=");
pw.print(mInterface.getInstallerPackageName(info.packageName));
}
/******/

继续看 getInstallerPackageName 的实现,位于:platform/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java 。

1
2
3
4
5
6
7
8
9
10
@Override
public String getInstallerPackageName(String packageName) {
final int callingUid = Binder.getCallingUid();
synchronized (mLock) {
final InstallSource installSource = getInstallSourceLocked(packageName, callingUid);
if (installSource == null) {
throw new IllegalArgumentException("Unknown package: " + packageName);
}
}
}

再追到 platform/frameworks/base/services/core/java/com/android/server/pm/Settings.java ,会发现 3 个文件,位于 /data/system 下。

1
2
3
mSettingsFilename = new File(mSystemDir, "packages.xml");
mBackupSettingsFilename = new File(mSystemDir, "packages-backup.xml");
mPackageListFilename = new File(mSystemDir, "packages.list");

使用root,读这里的文件,这里数据是有的,只是没有显示出来。

1
<package name="org.cocos2d.Dice" codePath="/data/app/~~iHhdolZdlOaWLmqtjOgq6Q==/org.cocos2d.Dice-drB5TRscmvfPl9HpP0UuQQ==" nativeLibraryPath="/data/app/~~iHhdolZdlOaWLmqtjOgq6Q==/org.cocos2d.Dice-drB5TRscmvfPl9HpP0UuQQ==/lib" primaryCpuAbi="arm64-v8a" publicFlags="940097092" privateFlags="-1409282048" ft="180992bd3e0" it="17fd5f6a55b" ut="180992bdb7c" version="65" userId="10074" installer="com.miui.packageinstaller" installInitiator="com.miui.packageinstaller" installOriginator="com.taptap">

Android 11 的新的 API getInstallSourceInfo,根据文档,缺少 INSTALL_PACKAGES 权限时,无法拿到 originatingPackageName。

If the calling application does not hold the INSTALL_PACKAGES permission then the result will always return null from InstallSourceInfo.getOriginatingPackageName().

看看具体实现,位于 platform/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java 。

代码大意为:先取回 InstallSource ,此时数据是完整的;然后 checkCallingOrSelfPermission INSTALL_PACKAGES ,对于无权限的调用者,PM 会主动将 originatingPackageName 置空,和文档描述一致,普通APP无法读到该字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public InstallSourceInfo getInstallSourceInfo(String packageName) {
final int callingUid = Binder.getCallingUid();
final int userId = UserHandle.getUserId(callingUid);

String installerPackageName;
String initiatingPackageName;
String originatingPackageName;

final InstallSource installSource;
synchronized (mLock) {
installSource = getInstallSourceLocked(packageName, callingUid);
if (installSource == null) {
return null;
}
}
/******/
if (originatingPackageName != null && mContext.checkCallingOrSelfPermission(
Manifest.permission.INSTALL_PACKAGES) != PackageManager.PERMISSION_GRANTED) {
originatingPackageName = null;
}
}

既然需要 INSTALL_PACKAGES 权限的话。。。

众所周知,INSTALL_PACKAGES 是一个 SignatureOrSystem,普通的 APP 是不可能获得这个权限的。但真的已经没有办法了吗?

众所周知啊,adb shell是唯一一个可以安装APP的用户,shell执行的代码开发者完全可控,既然 pm 命令可以直接被执行,说明 shell 是可以访问到 binder 的,计划通。

第一步:让 shell 执行任意的 java 代码

背景知识1:am、pm等命令,都是通过java实现的,细节见:https://www.cnblogs.com/wanghongzhu/p/15067133.html

背景知识2:开发一个shell里可以使用cli调用的apk,细节见:https://testerhome.com/topics/8622

第二步:拿到 binder

追一下 am 的代码,位于 frameworks/base/cmds/am/src/com/android/commands/am/Am.java ,直接开抄!

1
2
3
4
5
private IActivityManager mAm;
private IPackageManager mPm;

mAm = ActivityManager.getService();
mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));

第三步:自己写个 app

备注:IPackageManager 在 Android SDK 里没有,可以自己写个假的放在 apk 里用于编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
private static final String PROCESS_NAME = "helloworld.cli";

@RequiresApi(api = Build.VERSION_CODES.R)
public static void main(String[] args) throws RemoteException {
System.out.println("Usage of " + PROCESS_NAME);
IPackageManager mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
for (String arg : args) {
InstallSourceInfo info = mPm.getInstallSourceInfo(arg);
System.out.println("Package:" + arg);
System.out.println("getInstallingPackageName:" + info.getInstallingPackageName());
System.out.println("getInitiatingPackageName:" + info.getInitiatingPackageName());
System.out.println("getOriginatingPackageName:" + info.getOriginatingPackageName());
}
}
}

第四步:加载和验证

1
2
3
4
5
6
7
8
adb push /Users/leadroyal/Android_code/HelloAndroid/app/build/outputs/apk/debug/app-debug.apk /sdcard/
adb shell
gauguinpro:/ $ CLASSPATH=/sdcard/app-debug.apk app_process /system/bin com.leadroyal.helloandroid.Main org.cocos2d.Dice
Usage of helloworld.cli
Package:org.cocos2d.Dice
getInstallingPackageName:com.miui.packageinstaller
getInitiatingPackageName:com.miui.packageinstaller
getOriginatingPackageName:com.taptap

好的,完工!

结论

  • 小于等于 Androi 10,无解
  • 大于等于 Android 11,使用 shell 调用 getInstallSourceInfo
  • MIUI,需要 root 权限,读 provider