Android Hook 剪切板相关方法

news2024/11/25 7:09:43

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

结束!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/783843.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云原生容器内的一次pg_repack排错和解决过程

postgresql的pg_repack 这个cronjob一直执行不了。 排错过程: 用命令 kubectl describe job pg-repack-scheduler-manual-wv82r -n xxx没有查看用有用信息想办法进它启动的pod查看&#xff0c;于是在执行pg_repack.sh命令前&#xff0c;先加一个睡眠时间&#xff0c;如下: - …

Megatron-LM:Transformer模型专用分布式张量模型并行方法

论文标题&#xff1a;Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism 论文链接&#xff1a;https://arxiv.org/abs/1909.08053 论文来源&#xff1a;NVIDIA 一、概述 随着自然语言处理领域预训练语言模型的规模变得越来越大&#xff…

access跨库查询

服务器上面安装了安全狗、Waf这样的安全软件&#xff0c;没有办法下载数据库内容 都在同一个服务器的不同网站&#xff0c;从11查12的数据库 12数据库路径在C:\wwwtest\2AspCMS\AspCms_data 把data.asp后缀改成mdb就能看到里面的表了&#xff0c;data.mdb如下 语句 当前网站…

CSS自学框架之表格和项目列表

表格和项目列表很直观的显示数据&#xff0c;是我们web开发中经常遇到的最简单表现信息形式。具体代码如下&#xff1a; 一、css代码 ul,ol{margin-left: 1.25em;} /* - 表格 */.myth-table{width: 100%;overflow-x: auto;overflow-y: hidden;border-radius: var(--radius);…

《Pytorch深度学习和图神经网络(卷 2)》学习笔记——第二章

基于图片内容的处理任务 主要包括目标检测、图片分割两大任务。 目标检测&#xff1a;精度相对较高&#xff0c;主要是以检测框的方式&#xff0c;找出图片中目标物体所在坐标。模型运算量相对较小&#xff0c;相对较快。 图片分割&#xff1a;精度相对较低&#xff0c;主要是…

【工具-jmeter】jmeter 入门级 demo 练习

目录 前言&#xff1a; 1. Jmeter 准备 1.1 jmeter 安装包下载 1.2 jmeter 启动 1.3 jmeter 语言选择 2. Jmeter 运行 1 个 Web 请求的 demo 2.1 添加 1 个 Thread Group 线程组 2.2 添加 1 个 HTTP Request 请求 2.3 乱码问题 2.4 添加 1 个 HTTP Header 请求头 2.…

开发中遇到的 cookie 问题

1. cookie 无法跨域携带问题 尽管已经登录&#xff0c;但是请求接口返回状态码&#xff1a;202&#xff0c;msg&#xff1a; 未登录&#xff0c;如下图所示&#xff1b; 1.1 XMLHttpRequest.withCredentials未设置 如果需要跨域 AJAX 请求发送 Cookie&#xff0c;需要withCre…

【UE】虚幻网络同步

UE网络官方文档链接&#xff1a;https://docs.unrealengine.com/5.2/zh-CN/networking-overview-for-unreal-engine/ 虚幻的网络模式 服务器作为游戏主机&#xff0c;保留一个真实授权的游戏状态。换句话说&#xff0c;服务器是多人游戏实际发生的地方。客户端会远程控制其在服…

SpringBoot Redis 使用Lettuce和Jedis配置哨兵模式

Redis 从入门到精通【应用篇】之SpringBoot Redis 配置哨兵模式 Lettuce 和Jedis 文章目录 Redis 从入门到精通【应用篇】之SpringBoot Redis 配置哨兵模式 Lettuce 和Jedis前言Lettuce和Jedis区别1. 连接方式2. 线程安全性 教程如下1. Lettuce 方式配置1.1. 添加 Redis 和 Let…

Java项目里添加python解析器

java项目里配置了SDK为1.8&#xff0c;添加python文件时会无法解析。 提示让模块配置Python解析器&#xff0c;点击 配置python解析器 &#xff0c;弹出如下&#xff1a; 应用即可。

【机器学习】异常检测

异常检测 假设你是一名飞机涡扇引擎工程师&#xff0c;你在每个引擎出厂之前都需要检测两个指标——启动震动幅度和温度&#xff0c;查看其是否正常。在此之前你已经积累了相当多合格的发动机的出厂检测数据&#xff0c;如下图所示 我们把上述的正常启动的数据集总结为 D a t…

【Linux命令200例】chattr改变文件的扩展属性

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;本文已收录于专栏&#xff1a;Linux命令大全。 &#x1f3c6;本专栏我们会通过具体的系统的命令讲解加上鲜活的实操案例对各个命令进行深入…

【人工智能】博弈、极小极大值、α-β剪枝、截断测试

文章目录 博弈极小极大值α-β剪枝截断测试博弈 极小极大值 假设两个玩家都以最大化自身利用进行博弈举例: 计算机假设在它移动后,对手会选择最小化的行动计算机在考虑自己的行动和对手的最佳行动后选择最佳行动算法实现

【python】在matlab中调用python

参考 Matlab调用Python - 知乎 (zhihu.com) 说一下我犯的错误&#xff1a; 1、电脑上有没有python都可以&#xff0c;我以为anaconda里的python不行&#xff0c;又重新下了一个python3.8 实际上导入的时候可以用 pyversion(D:\myDownloads\anaconda\envs\pytorch38\pytho…

Docker 全栈体系(五)

Docker 体系&#xff08;高级篇&#xff09; 二、DockerFile解析 1. 是什么&#xff1f; Dockerfile是用来构建Docker镜像的文本文件&#xff0c;是由一条条构建镜像所需的指令和参数构成的脚本。 1.1 概述 1.2 官网 https://docs.docker.com/engine/reference/builder/ 1…

freeBSD:ssh登录root

/etc/inetd.conf ee /etc/inetd.conf 去掉# /etc/rc.conf ee /etc/rc.conf 添加一句 sshd_enable"YES" /etc/ssh/sshd_config vi /etc/ssh/sshd_config 22行可以修改端口号&#xff0c;非必要就默认22 36行 去掉# 后面修改成 yes 61 PasswordAuthentication…

Python处理Elasticsearch

简介&#xff1a;Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性&#xff0c;能使数据在生产环境变得更有价值。Elasticsearch 的实现原理主要分为以下几个步骤&…

Golang数据库连接池技术原理与实现

1 为什么需要连接池&#xff1f; 如果不用连接池&#xff0c;而是每次请求都创建一个连接是比较昂贵的&#xff0c;因此需要完成3次tcp握手。同时在高并发场景下&#xff0c;由于没有连接池的最大连接数限制&#xff0c;可以创建无数个连接&#xff0c;耗尽文件描述符。连接池…

【软件测试】什么是selenium

1.seleniumJava环境搭建 前置条件: Java最低版本要求为8,浏览器使用chrome浏览器 1.1下载chrome浏览器 https://www.google.cn/chrome/ 1.2查看浏览器版本 点击关于Google chrome. 记住版本的前三个数. 1.3下载浏览器驱动 http://chromedriver.chromium.org/downloads 下载…