想起之前做过的项目有安全合规要求:主动弹窗获取用户同意了才能调用剪切板相关方法,否则属于违规调用,如果是自己项目的相关调用可以自己加一层if判断
但是一些第三方的jar包里面也有在调用的话,我们就无能为力了,而且整个项目的所有调用处都一个一个去加判断的话,就会显得很麻烦,这里用Hook方法完成拦截方法调用+判断
先要理清 clipboardManager.getPrimaryClip()方法内部的逻辑:
Android11的ClipboardManager源码:http://aospxref.com/android-11.0.0_r21/xref/frameworks/base/core/java/android/content/ClipboardManager.java
里面是这样的:
其实就是调用的是mService的相关方法,而mService其实是ServiceManager里面剪切板相关方法封装后的接口,所以到了这里自然就想到用反射拿到这个mService字段,替换成我们自定义的代理类就可以了
1. 在application的onCreate里面执行hook方法,确保后面的调用都能生效
2. inject方法
到这里为止,后面在其他地方调用 getPrimaryClip()方法都会走到我们设置的代理类方法里面进行拦截。
注意,在Android9+的手机上运行,会碰到拿不到反射字段的情况,不信你们自己试一试
关于android9+的反射限制,可以百度查看其他人的分析,我这里用网上大神的解决方案:
1. 在根目录的build.gradle里面加这个仓库地址:
2. 在app的build.gradle脚本里面加这个依赖
implementation 'com.github.ChickenHook:RestrictionBypass:2.2'
sync一下,就搞定了,虽然as还是会标红提示你,但是可以无视直接build
但是!!!注意!!!
我开始也以为到这里就结束了,但是运行后发现,根本没有起作用
activity用法:
我们在其他地方调用一般是这样子调用的对吧,但是经过我断点发现,这里拿到的clipboardManager和之前inject方法里面拿到的clipboardManager实例根本就不是同一个!!!
自然里面的mService实例也不是我们修改过的那个代理类,所以才会不起作用
所以我们就要搞清楚这个clipboardManager到底是怎么获得的
省略断点,跟踪流程发现,实际上调用的是SystemServiceRegistry类的getSystemService方法
主要就是拿到fetcher,调用fetcher.getService(ctx)方法返回给我们
fetcher是来自SYSTEM_SERVICE_FETCHERS,发现这个字段就是个map
那么这个map是在哪里进行put的呢,在当前文件全局搜索发现只有这里put了东西进去
而这个registerService方法我们可以在开头的静态代码块里面发现调用
包括我们想要的clipboardManager
也就是说 SystemServiceRegistry类初始化的时候,这里就put进去了值
因为SYSTEM_SERVICE_FETCHERS字段是个static字段,所以整个app进程只会有一个且只会执行一次初始化的操作,所以无论我们传入的context是Application的还是Activity,拿到的都是同一个fetcher实例,那么问题只能出在fetcher.getService(ctx)方法里面
继续跟踪fetcher.getService(ctx)方法,断点进去
下面是具体方法的实现,方法有点长,直接看下面分析结论
/*** Override this class when the system service constructor needs a* ContextImpl and should be cached and retained by that context.*/static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {private final int mCacheIndex;CachedServiceFetcher() {// Note this class must be instantiated only by the static initializer of the// outer class (SystemServiceRegistry), which already does the synchronization,// so bare access to sServiceCacheSize is okay here.mCacheIndex = sServiceCacheSize++;}@Override@SuppressWarnings("unchecked")public final T getService(ContextImpl ctx) {final Object[] cache = ctx.mServiceCache;final int[] gates = ctx.mServiceInitializationStateArray;boolean interrupted = false;T ret = null;for (;;) {boolean doInitialize = false;synchronized (cache) {// Return it if we already have a cached instance.T service = (T) cache[mCacheIndex];if (service != null || gates[mCacheIndex] == ContextImpl.STATE_NOT_FOUND) {ret = service;break; // exit the for (;;)}// If we get here, there's no cached instance.// Grr... if gate is STATE_READY, then this means we initialized the service// once but someone cleared it.// We start over from STATE_UNINITIALIZED.if (gates[mCacheIndex] == ContextImpl.STATE_READY) {gates[mCacheIndex] = ContextImpl.STATE_UNINITIALIZED;}// It's possible for multiple threads to get here at the same time, so// use the "gate" to make sure only the first thread will call createService().// At this point, the gate must be either UNINITIALIZED or INITIALIZING.if (gates[mCacheIndex] == ContextImpl.STATE_UNINITIALIZED) {doInitialize = true;gates[mCacheIndex] = ContextImpl.STATE_INITIALIZING;}}if (doInitialize) {// Only the first thread gets here.T service = null;@ServiceInitializationState int newState = ContextImpl.STATE_NOT_FOUND;try {// This thread is the first one to get here. Instantiate the service// *without* the cache lock held.service = createService(ctx);newState = ContextImpl.STATE_READY;} catch (ServiceNotFoundException e) {onServiceNotFound(e);} finally {synchronized (cache) {cache[mCacheIndex] = service;gates[mCacheIndex] = newState;cache.notifyAll();}}ret = service;break; // exit the for (;;)}// The other threads will wait for the first thread to call notifyAll(),// and go back to the top and retry.synchronized (cache) {// Repeat until the state becomes STATE_READY or STATE_NOT_FOUND.// We can't respond to interrupts here; just like we can't in the "doInitialize"// path, so we remember the interrupt state here and re-interrupt later.while (gates[mCacheIndex] < ContextImpl.STATE_READY) {try {// Clear the interrupt state.interrupted |= Thread.interrupted();cache.wait();} catch (InterruptedException e) {// This shouldn't normally happen, but if someone interrupts the// thread, it will.Slog.w(TAG, "getService() interrupted");interrupted = true;}}}}if (interrupted) {Thread.currentThread().interrupt();}return ret;}
我们可以发现会先从ctx.mServiceCache这个缓存数组里面找,找不到就去执行createService方法:
而createService就是最开始static代码块里面传入的实例方法:
ctx.getOuterContext拿到的实例其实就是Application的context
到这里我们可以发现,因为我们传入的context不同,导致拿到的缓存数组也不同,就会走到createService方法去创建实例。
但是实例化方法传入的参数是一样的,都是传入Application的context和主线程的handler。导致我们拿到的clipboardManager实例是经过相同的构造方法和构造参数构造出来的不同实例。
分析了获得clipboardManager实例的获得过程,我们就可以找地方下手了
既然实例是从fetcher.getService方法中返回的,那我们只要拦截这个方法,让它返回同一个实例,就可以解决问题了
先用反射拿到这个map
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName("android.app.SystemServiceRegistry");Field[] fields = clazz.getDeclaredFields();System.out.println(fields[1].getName());@SuppressLint("BlockedPrivateApi") Field field = clazz.getDeclaredField("SYSTEM_SERVICE_FETCHERS");field.setAccessible(true);ArrayMap objs = (ArrayMap) field.get(null);
不知道为什么这个SystemServiceRegistry类没有办法import,只能通过全路径的方式来反射加载了
拿到clipboard对应的fetcher,然后塞入我们修改过的代理类进去
注意这里要在try catch下完成,最开始的context传入的也是application的context
这里是完整实现代码,主要先执行替换fetcher的代理类,再进行clipboardManager相关方法的代理替换
public void inject(Context context){ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);try {@SuppressLint({"BlockedPrivateApi", "PrivateApi"}) Field field = Class.forName("android.app.SystemServiceRegistry").getDeclaredField("SYSTEM_SERVICE_FETCHERS");field.setAccessible(true);ArrayMap objs = (ArrayMap) field.get(null);Object fetcher = objs.get("clipboard");@SuppressLint("PrivateApi") Class<?> clazz = Class.forName("android.app.SystemServiceRegistry$ServiceFetcher");objs.put("clipboard", Proxy.newProxyInstance(context.getClassLoader(), new Class[]{clazz}, (proxy, method, args) -> {if (method.getName().equals("getService")){return clipboardManager;}else {return method.invoke(fetcher, args);}}));} catch (Exception e) {throw new RuntimeException(e);}try{boolean isAgreed = true;//通过反射拿到mService字段@SuppressLint("SoonBlockedPrivateApi") Field mServiceField = ClipboardManager.class.getDeclaredField("mService");mServiceField.setAccessible(true);Object mService = mServiceField.get(clipboardManager);Class clazz = Class.forName("android.content.IClipboard");//生成代理类Object proxyInstance = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//生成的代理类,判断如果是调用getPrimaryClip方法的话,加上是否用户同意过的逻辑,这里我用true代替了if (method.getName().equals("getPrimaryClip") && isAgreed){System.out.println("hhh, 不准调,没授权!!!");return null;}return method.invoke(mService,args);}});//将该代理类塞回去@SuppressLint("SoonBlockedPrivateApi") Field sServiceField = ClipboardManager.class.getDeclaredField("mService");sServiceField.setAccessible(true);sServiceField.set(clipboardManager, proxyInstance);}catch (Exception e){e.printStackTrace();}}
结束!