想起之前做过的项目有安全合规要求:主动弹窗获取用户同意了才能调用剪切板相关方法,否则属于违规调用,如果是自己项目的相关调用可以自己加一层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 implements ServiceFetcher {
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() {
@Override
public 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();
}
}
结束!