Android Hook 剪切板相关方法

想起之前做过的项目有安全合规要求:主动弹窗获取用户同意了才能调用剪切板相关方法,否则属于违规调用,如果是自己项目的相关调用可以自己加一层if判断

Android Hook 剪切板相关方法_第1张图片

但是一些第三方的jar包里面也有在调用的话,我们就无能为力了,而且整个项目的所有调用处都一个一个去加判断的话,就会显得很麻烦,这里用Hook方法完成拦截方法调用+判断

先要理清 clipboardManager.getPrimaryClip()方法内部的逻辑:

Android11的ClipboardManager源码:http://aospxref.com/android-11.0.0_r21/xref/frameworks/base/core/java/android/content/ClipboardManager.java

里面是这样的:

Android Hook 剪切板相关方法_第2张图片

 Android Hook 剪切板相关方法_第3张图片

其实就是调用的是mService的相关方法,而mService其实是ServiceManager里面剪切板相关方法封装后的接口,所以到了这里自然就想到用反射拿到这个mService字段,替换成我们自定义的代理类就可以了

1. 在application的onCreate里面执行hook方法,确保后面的调用都能生效

Android Hook 剪切板相关方法_第4张图片

2. inject方法

Android Hook 剪切板相关方法_第5张图片

到这里为止,后面在其他地方调用 getPrimaryClip()方法都会走到我们设置的代理类方法里面进行拦截。

注意,在Android9+的手机上运行,会碰到拿不到反射字段的情况,不信你们自己试一试

关于android9+的反射限制,可以百度查看其他人的分析,我这里用网上大神的解决方案:

1. 在根目录的build.gradle里面加这个仓库地址:

Android Hook 剪切板相关方法_第6张图片

2. 在app的build.gradle脚本里面加这个依赖

implementation 'com.github.ChickenHook:RestrictionBypass:2.2'

sync一下,就搞定了,虽然as还是会标红提示你,但是可以无视直接build

但是!!!注意!!!

我开始也以为到这里就结束了,但是运行后发现,根本没有起作用

activity用法:

Android Hook 剪切板相关方法_第7张图片

我们在其他地方调用一般是这样子调用的对吧,但是经过我断点发现,这里拿到的clipboardManager和之前inject方法里面拿到的clipboardManager实例根本就不是同一个!!!

自然里面的mService实例也不是我们修改过的那个代理类,所以才会不起作用

所以我们就要搞清楚这个clipboardManager到底是怎么获得的

省略断点,跟踪流程发现,实际上调用的是SystemServiceRegistry类的getSystemService方法Android Hook 剪切板相关方法_第8张图片

主要就是拿到fetcher,调用fetcher.getService(ctx)方法返回给我们

fetcher是来自SYSTEM_SERVICE_FETCHERS,发现这个字段就是个map

那么这个map是在哪里进行put的呢,在当前文件全局搜索发现只有这里put了东西进去

Android Hook 剪切板相关方法_第9张图片

而这个registerService方法我们可以在开头的静态代码块里面发现调用

Android Hook 剪切板相关方法_第10张图片

包括我们想要的clipboardManager

Android Hook 剪切板相关方法_第11张图片

也就是说 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方法:

Android Hook 剪切板相关方法_第12张图片

Android Hook 剪切板相关方法_第13张图片

而createService就是最开始static代码块里面传入的实例方法:

Android Hook 剪切板相关方法_第14张图片

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,只能通过全路径的方式来反射加载了

Android Hook 剪切板相关方法_第15张图片

拿到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();
        }
    }

结束!

你可能感兴趣的:(android,hook)