应用安全 --- apk加固 之 已安装应用检查
// 这个代码的主要作用是: // 应用互斥检查机制: // 检查设备上是否安装了包名为"com.gklbb"的应用 // 如果检测到该应用已安装,立即关闭当前应用 // 如果未安装,应用正常运行 // 可能的使用场景: // 防止重复安装: // 防止用户同时安装同一应用的不同版本 // 确保只有一个版本在设备上运行 // 许可证检查: // "com.gklbb"可能是一个许可证验证应用 // 只有安装了特定的许可证应用,当前应用才能运行 // 应用冲突避免: // 避免与特定应用产生功能冲突 // 确保应用生态的稳定性 // 反调试/反分析: // 检测是否安装了特定的调试工具或分析工具 // 如果检测到,立即退出以防止被分析 public class MainActivity extends AppCompatActivity { // 在MainActivity的onCreate或onResume方法中调用此方法 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 调用检查方法 isInstalled(); } // 或者在onResume中调用 @Override protected void onResume() { super.onResume(); isInstalled(); } /** * 检查特定应用是否已安装的方法 * 如果目标应用已安装,则关闭当前应用 */ public void isInstalled() { try { // 获取PackageManager实例 PackageManager packageManager = getPackageManager(); // 尝试获取包名为"com.gklbb"的应用信息 // 第二个参数1表示GET_ACTIVITIES标志 PackageInfo packageInfo = packageManager.getPackageInfo("com.gklbb", 1); // 如果能成功获取到包信息(没有抛出异常),说明应用已安装 // 立即关闭当前应用 finish(); } catch (Exception e) { // 如果抛出异常(通常是NameNotFoundException),说明应用未安装 // 什么都不做,让应用继续运行 return; } } }
package com.iran.SmaliHelper; // 定义包名,这是代码的命名空间 // 导入Android系统相关的类库 import android.app.Application; // 导入应用程序基类 import android.content.Context; // 导入上下文类,用于访问系统服务 import android.content.pm.PackageInfo; // 导入包信息类,包含应用的详细信息 import android.content.pm.PackageManager; // 导入包管理器类,用于管理应用包 import android.content.pm.Signature; // 导入签名类,用于应用签名验证 import android.util.Base64; // 导入Base64编码解码工具 import java.io.ByteArrayInputStream; // 导入字节数组输入流 import java.io.DataInputStream; // 导入数据输入流 import java.lang.reflect.Field; // 导入反射字段类,用于访问类的字段 import java.lang.reflect.InvocationHandler; // 导入调用处理器接口,用于动态代理 import java.lang.reflect.Method; // 导入反射方法类,用于调用方法 import java.lang.reflect.Proxy; // 导入代理类,用于创建动态代理对象 import java.util.ArrayList; // 导入数组列表类 // 定义一个最终类,继承Application并实现InvocationHandler接口 // 这个类的作用是拦截和修改系统的包管理器调用 public final class RemoveSoftware extends Application implements InvocationHandler { private String appClassName = "com.GKLBB "; // 存储应用类名 private String data = ""; // 存储签名数据的Base64编码字符串 private Object sPackageManager; // 保存原始的系统包管理器对象 private byte[][] sign; // 存储伪造的签名数据 private String type = "2"; // 工作模式:1=修改版本号,2=隐藏应用,3=伪造签名 // 构造函数,调用父类构造函数 public RemoveSoftware() { super(); // 调用Application的构造函数 } @Override // 重写attachBaseContext方法,这是应用启动时最早调用的方法之一 protected void attachBaseContext(Context base) { super.attachBaseContext(base); // 调用父类方法 try { // 通过反射获取ActivityThread类(Android系统的主线程类) Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); // 获取当前ActivityThread实例的方法 Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); // 调用静态方法获取当前ActivityThread实例 Object currentActivityThread = currentActivityThreadMethod.invoke(null); // 获取ActivityThread类中的sPackageManager字段(系统包管理器) Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager"); sPackageManagerField.setAccessible(true); // 设置为可访问(绕过private限制) this.sPackageManager = sPackageManagerField.get(currentActivityThread); // 保存原始包管理器 // 获取IPackageManager接口类(包管理器的接口) Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager"); // 创建动态代理对象,用我们的类作为调用处理器 Object proxy = Proxy.newProxyInstance( iPackageManagerInterface.getClassLoader(), // 使用接口的类加载器 new Class[]{iPackageManagerInterface}, // 要代理的接口 this // 使用当前对象作为调用处理器 ); // 用我们的代理对象替换ActivityThread中的原始包管理器 sPackageManagerField.set(currentActivityThread, proxy); // 同时替换PackageManager中的mPM字段 PackageManager pm = base.getPackageManager(); // 获取包管理器实例 Field mPmField = pm.getClass().getDeclaredField("mPM"); // 获取内部的mPM字段 mPmField.setAccessible(true); // 设置为可访问 mPmField.set(pm, proxy); // 用代理对象替换 } catch (Exception e) { e.printStackTrace(); // 如果出错就打印错误信息 } } // 反射工具方法:通过类名和对象获取指定字段的值 public Object getFieldObject(String className, Object obj, String fieldName) { try { Class<?> objClass = Class.forName(className); // 根据类名获取Class对象 Field field = objClass.getDeclaredField(fieldName); // 获取指定名称的字段 field.setAccessible(true); // 设置字段为可访问(绕过private等限制) return field.get(obj); // 从对象中获取字段值并返回 } catch (Exception e) { e.printStackTrace(); // 出错时打印错误信息 return null; // 返回null表示获取失败 } } // 反射工具方法:获取类的静态字段值 public Object getStaticFieldObject(String className, String fieldName) { try { Class<?> objClass = Class.forName(className); // 根据类名获取Class对象 Field field = objClass.getDeclaredField(fieldName); // 获取指定名称的字段 field.setAccessible(true); // 设置字段为可访问 return field.get(null); // 获取静态字段值(传入null因为是静态字段) } catch (Exception e) { e.printStackTrace(); // 出错时打印错误信息 return null; // 返回null表示获取失败 } } @Override // 动态代理的核心方法:拦截所有对包管理器的调用 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // 获取被调用的方法名 // 模式1:修改版本号模式 if ("1".equals(type)) { // 如果调用的是getPackageInfo方法,且查询的是当前应用 if ("getPackageInfo".equals(methodName) && getPackageName().equals(args[0])) { // 先调用原始方法获取真实的包信息 PackageInfo info = (PackageInfo) method.invoke(sPackageManager, args); info.versionCode = Integer.MAX_VALUE; // 把版本号改为最大值(防检测) return info; // 返回修改后的包信息 } } // 模式2:隐藏应用模式 else if ("2".equals(type)) { // 如果调用getInstalledPackages(获取已安装应用列表) if ("getInstalledPackages".equals(methodName)) { return new ArrayList<>(); // 返回空列表,让系统以为没有安装任何应用 } // 对于查询当前应用的各种方法,都把包名替换成假的 if (isCurrentPackageQuery(methodName, args)) { args[0] = "ccc"; // 把真实包名替换为"ccc",让查询失败 } } // 模式3:签名伪造模式 else if ("3".equals(type)) { initializeSignatures(); // 初始化伪造的签名数据 // 如果调用getPackageInfo方法 if ("getPackageInfo".equals(methodName)) { String pkgName = (String) args[0]; // 获取要查询的包名 Integer flag = (Integer) args[1]; // 获取查询标志 // 如果请求签名信息(flag包含0x40)且查询的是当前应用 if ((flag & 0x40) != 0 && getPackageName().equals(pkgName)) { // 先获取真实的包信息 PackageInfo info = (PackageInfo) method.invoke(sPackageManager, args); // 用伪造的签名替换真实签名 info.signatures = new Signature[sign.length]; for (int i = 0; i < info.signatures.length; i++) { info.signatures[i] = new Signature(sign[i]); // 创建伪造的签名对象 } return info; // 返回包含伪造签名的包信息 } } } // 如果不需要特殊处理,就调用原始的包管理器方法 return method.invoke(sPackageManager, args); } // 判断是否是查询当前应用的方法调用 private boolean isCurrentPackageQuery(String methodName, Object[] args) { if (args == null || args.length == 0) return false; // 如果没有参数就返回false // 定义需要拦截的方法名列表(这些都是查询应用信息的方法) String[] targetMethods = { "getPackageInfo", "getApplicationInfo", "getPackageUid", "getPackageGids", "getApplicationEnabledSetting", "getText", "getResourcesForApplication", "getLaunchIntentForPackage", "getInstallerPackageName" }; // 遍历目标方法列表 for (String targetMethod : targetMethods) { // 如果方法名匹配且第一个参数是当前应用的包名 if (targetMethod.equals(methodName) && getPackageName().equals(args[0])) { return true; // 返回true表示需要拦截 } } // 特殊处理获取应用图标和横幅的方法 if (("getApplicationBanner".equals(methodName) || "getApplicationIcon".equals(methodName)) && args[0] instanceof String && // 确保第一个参数是字符串 getPackageName().equals(args[0])) { // 且是当前应用包名 return true; // 返回true表示需要拦截 } return false; // 其他情况返回false,不需要拦截 } // 初始化伪造的签名数据 private void initializeSignatures() { if (sign != null) return; // 如果已经初始化过就直接返回 try { // 创建数据输入流,从Base64解码的data字符串中读取签名数据 DataInputStream is = new DataInputStream( new ByteArrayInputStream(Base64.decode(data, 0)) // 将Base64字符串解码为字节数组 ); int signCount = is.read() & 0xff; // 读取签名数量(一个字节) sign = new byte[signCount][]; // 创建二维字节数组存储签名 // 循环读取每个签名的数据 for (int i = 0; i < sign.length; i++) { int length = is.readInt(); // 读取当前签名的长度(4个字节) sign[i] = new byte[length]; // 创建对应长度的字节数组 is.readFully(sign[i]); // 读取完整的签名数据 } } catch (Exception e) { e.printStackTrace(); // 如果出错就打印错误信息 } } } }
"偷梁换柱"的魔术
想象一下,Android系统就像一个大公司:
原本的情况:
IPackageManager = 公司的"包裹管理部门"接口规范
真正的包管理器 = 负责处理所有App信息查询的"真实员工"
ActivityThread = 公司的"总调度中心",所有请求都通过它
这段代码做了什么:
找到真实员工
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
先获取"包裹管理部门"的工作规范(接口定义)
制造一个"替身"
Object proxy = Proxy.newProxyInstance(...)
创建一个假员工(代理对象)
这个假员工看起来和真员工一模一样(实现相同接口)
但实际上所有工作都会转给我们的处理器(this)
偷梁换柱
sPackageManagerField.set(currentActivityThread, proxy);
把公司总调度中心里的"真员工"换成我们的"假员工"
从此以后,所有对包管理器的请求都会先经过我们
🔄 工作流程:
App请求查询包信息
↓
总调度中心(ActivityThread)
↓
我们的假员工(proxy)
↓
我们的处理器(invoke方法) ← 在这里可以篡改数据!
↓
真正的员工(原始PackageManager)
↓
返回结果给App
💡 核心原理:
动态代理模式:创建一个中间人,拦截所有方法调用
反射机制:在运行时修改系统内部的对象引用
接口统一性:代理对象和原对象实现相同接口,外界无法察觉
这样,当任何App(包括系统)想查询包信息时,都会先经过我们的"假员工",我们就可以在invoke方法里随意修改返回的数据,
举个例子:
假设设备上有两个应用:
-
应用A:就是集成了这段
RemoveSoftware
代码的应用。 -
应用B:是一个安全检查应用,它想扫描设备上安装了哪些应用。
-
场景1(应用A未启动):
-
应用B 调用
getInstalledPackages()
。这个调用由系统服务处理,返回真实的、完整的应用列表,其中包括应用A。代码未生效。
-
-
场景2(应用A已启动):
-
应用A 的进程启动,
attachBaseContext
被执行,成功Hook了它自己进程内的包管理器。 -
现在,如果应用A 自己 调用
getInstalledPackages()
,这个调用会被自己的invoke
方法拦截,并返回一个空列表new ArrayList<>()
。 -
但是,应用B 调用
getInstalledPackages()
仍然不受影响,会拿到完整列表。代码只对应用A自身生效。
-
结论
这段代码是一种 “自欺欺人” 式的隐藏技术。它只能在一个已经运行的应用程序进程内部欺骗自己,让自己无法通过标准API看到真实的应用信息。它无法在全局范围内隐藏应用,也无法阻止其他应用程序或系统服务发现它。
要实现真正的全局隐藏,需要更高层次的权限和技术,例如修改系统框架(需要刷入自定义ROM)、使用Root权限直接修改系统服务的数据、或者利用Android系统本身的漏洞(通常会被在新版本中修复)。这段代码不属于上述任何一种情况。