文章目录
背景
最近研究隐私合规检测工作的时候有个需求是需要对应用收集用户设备的安装列表这个行为进行检测,现在简单记录下常用的获取设备安装列表的方法。
先搞清楚一个问题,为什么有些应用要获取设备安装的应用列表,目的是什么?
应用场景
1.应用间相互拉起
这种可能是应用联盟玩应用矩阵保活,比如你安装了应用A,它会主动读取手机中是否安装了兄弟应用B,如果有安装,那么它就可以主动唤醒应用B,还有一种正常点的就是应用A如果需要使用支付功能或地图导航功能,但它自己没有实现,那么就可唤起设备中有支付功能或地图导航功能的应用来完成。
2.大数据统计、精准推送
这种主要是对设备中安装的应用列表进行获取并上传到云端,一是用来做应用安装量的数据统计,二是基于这些应用来判断用户兴趣爱好,对用户进行画像。有了用户喜好后期就便于广告的精准推送。
3.应用市场、清理、杀毒类应用
这类目的主要是一些工具类产品,比如应用市场通过获取设备中安装应用的信息来检查应用是否需要升级更新,清理杀毒类应用通过获取安装列表并通过云端的匹配来判断用户设备是否安全,是否安装了病毒、盗版软件等。
4.风控SDK
还有一种目的主要是用于应用做风控,应用通过获取设备中安装的应用来判断设备是否为模拟器、用户是否为正常用户。如果应用发现自己在模拟器中运行那么就启动主动防御功能,比如运行错误的逻辑又或者直接退出等。
获取方案
知道了获取应用安装列表的目的后来看看一些常见的获取应用列表的方案。
1.通过命令获取安装列表
private List<String> getPkgListFromShell_PM() {
List<String> packages = new ArrayList<String>();
try {
Process p = Runtime.getRuntime().exec("pm list packages");
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), "utf-8"));
String line = br.readLine();
while (line != null) {
line = line.trim();
if (line.length() > 8) {
String prefix = line.substring(0, 8);
if (prefix.equalsIgnoreCase("package:")) {
line = line.substring(8).trim();
if (!TextUtils.isEmpty(line)) {
Log.d("Demo", "install: " + line);
packages.add(line);
}
}
}
line = br.readLine();
}
br.close();
p.destroy();
} catch (Throwable t) {
t.printStackTrace();
}
return packages;
}
2.通过API获取安装列表
private List<String> getPkgListFromAPI_getInstalledPackages(Context context) {
List<String> packages = new ArrayList<String>();
try {
List<PackageInfo> packageInfos = context.getPackageManager().getInstalledPackages(PackageManager.GET_ACTIVITIES |
PackageManager.GET_SERVICES);
for (PackageInfo info : packageInfos) {
String pkg = info.packageName;
Log.d("Demo", "install: " + pkg);
packages.add(pkg);
}
} catch (Throwable t) {
t.printStackTrace();
}
return packages;
}
private List<String> getPkgListFromAPI_queryIntentActivities(Context context) {
List<String> packages = new ArrayList<String>();
try {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> mResolveInfos = context.getPackageManager().queryIntentActivities(intent,PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo info : mResolveInfos) {
String appName = info.activityInfo.applicationInfo.loadLabel(context.getPackageManager()).toString();
String appPkg = info.activityInfo.packageName;
Log.d("Demo", "install: " + appName + " " + appPkg);
packages.add(appPkg);
}
} catch (Throwable t) {
t.printStackTrace();
}
return packages;
}
private List<String> getPkgListFromAPI_getInstalledApplications(Context context) {
List<String> packages = new ArrayList<String>();
try {
List<ApplicationInfo> mApplicationInfo = context.getPackageManager().getInstalledApplications(PackageManager.GET_META_DATA);
for (ApplicationInfo info : mApplicationInfo) {
String appName = info.name;
String appPkg = info.packageName;
Log.d("Demo", "install: " + appPkg);
packages.add(appPkg);
}
} catch (Throwable t) {
t.printStackTrace();
}
return packages;
}
3.通过UID爆破应用列表
private boolean getPkgListFromAPI_getPackagesForUid(Context context){
for (int uid=10001;uid<30000;uid++){
String[] uidToPackage = context.getPackageManager().getPackagesForUid(uid);
if(uidToPackage != null){
Log.d("Hook",uid + " " + uidToPackage[0]);
}
}
return true;
}
4.根据包名反向获取应用列表
private boolean getPkgListFromAPI_getLaunchIntentForPackage(Context context){
List<String> appPkgList = new ArrayList<>();
appPkgList.add("com.google.android.apps.docs");
appPkgList.add("com.google.android.apps.docs2");
appPkgList.add("com.google.android.calendar");
for (String pkg:appPkgList){
Intent intent = context.getPackageManager().getLaunchIntentForPackage(pkg);
String status = intent == null?" 不存在":" 存在";
Log.d("Demo",pkg + status);
}
return true;
}
private boolean getPkgListFromAPI_getPackageInfo(Context context){
List<String> appPkgList = new ArrayList<>();
appPkgList.add("com.google.android.apps.docs");
appPkgList.add("com.google.android.apps.docs2");
appPkgList.add("com.google.android.calendar");
for (String pkg:appPkgList){
boolean flag = false;
try {
context.getPackageManager().getPackageInfo(pkg, PackageManager.GET_GIDS);
flag = true;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
String status = flag ?" 存在":" 不存在";
Log.d("Demo",pkg + status);
}
return true;
}
检测方案
知道了常见获取应用安装列表的方法后通过编写相应的hook代码来对这些关键API进行监控,以此来完成对应用获取安装列表的行为进行监控。
hook的实现可以使用Xposed、Frida或修改系统源码都可以,这里直接使用Frida来实现该功能,不熟悉Frida的小伙伴可以看看之前的文章Android逆向-Frida入门学习笔记
代码
Java.perform(function() {
var cn = "android.app.ApplicationPackageManager"; //因为PackageManager类是个抽象类,ApplicationPackageManager实现了该抽象类,所以Hook上ApplicationPackageManager即可
var appPackageManager = Java.use(cn);
if (appPackageManager) {
//Hook getInstalledPackages
var overloadcount = appPackageManager["getInstalledPackages"].overloads.length;
for (var i=0; i<overloadcount; i++){
appPackageManager["getInstalledPackages"].overloads[i].implementation = function () {
if(arguments.length){
for(var j=0; j<arguments.length; j++){
console.log("arg["+ j +"]: " + arguments[j]);
}
}
console.log("call " + cn + "->getInstalledPackages");
return this["getInstalledPackages"].apply(this, arguments);
};
}
//Hook queryIntentActivities
var overloadcount = appPackageManager["queryIntentActivities"].overloads.length;
for (var i=0; i<overloadcount; i++){
appPackageManager["queryIntentActivities"].overloads[i].implementation = function () {
if(arguments.length){
for(var j=0; j<arguments.length; j++){
console.log("arg["+ j +"]: " + arguments[j]);
}
}
console.log("call " + cn + "->queryIntentActivities");
return this["queryIntentActivities"].apply(this, arguments);
};
}
//Hook getInstalledApplications
var overloadcount = appPackageManager["getInstalledApplications"].overloads.length;
for (var i=0; i<overloadcount; i++){
appPackageManager["getInstalledApplications"].overloads[i].implementation = function () {
if(arguments.length){
for(var j=0; j<arguments.length; j++){
console.log("arg["+ j +"]: " + arguments[j]);
}
}
console.log("call " + cn + "->getInstalledApplications");
return this["getInstalledApplications"].apply(this, arguments);
};
}
//Hook getPackagesForUid
var overloadcount = appPackageManager["getPackagesForUid"].overloads.length;
for (var i=0; i<overloadcount; i++){
appPackageManager["getPackagesForUid"].overloads[i].implementation = function () {
if(arguments.length){
for(var j=0; j<arguments.length; j++){
console.log("arg["+ j +"]: " + arguments[j]);
}
}
console.log("call " + cn + "->getPackagesForUid");
return this["getPackagesForUid"].apply(this, arguments);
};
}
//Hook getLaunchIntentForPackage
var overloadcount = appPackageManager["getLaunchIntentForPackage"].overloads.length;
for (var i=0; i<overloadcount; i++){
appPackageManager["getLaunchIntentForPackage"].overloads[i].implementation = function () {
if(arguments.length){
for(var j=0; j<arguments.length; j++){
console.log("arg["+ j +"]: " + arguments[j]);
}
}
console.log("call " + cn + "->getLaunchIntentForPackage");
return this["getLaunchIntentForPackage"].apply(this, arguments);
};
}
//hook getPackageInfo
var overloadcount = appPackageManager["getPackageInfo"].overloads.length;
for (var i=0; i<overloadcount; i++){
appPackageManager["getPackageInfo"].overloads[i].implementation = function () {
if(arguments.length){
for(var j=0; j<arguments.length; j++){
console.log("arg["+ j +"]: " + arguments[j]);
}
}
console.log("call " + cn + "->getPackageInfo");
//return this["getPackageInfo"].apply(this, arguments);
};
}
}
});