Android开发:利用Xposed禁止应用访问手机联系人信息

手机中的联系人信息算得上是我们手机中的一大隐私了,我们当然不希望手机中的联系人信息被窥探被泄露出去,让人不能如愿的是,目前好多的App都会要求阅读手机联系人信息权限,当然那些大厂我们就不说了,比如QQ微信,还有支付宝,毕竟他们已经是我们生活离不开的应用了,只能期望他们大厂会保护我们的隐私,可恶的是一些小厂的App也厚无颜耻的要求阅读手机联系人权限,这就很让人无语了!

所以今天给大家讲讲该怎么禁止应用访问手机联系人信息,保护我们的隐私不容侵犯~

首先老样子,我们还是来分析一下获取手机联系人的途径。平常我们大多通过内容提供器来获取联系人信息,例如博主下面的代码:

Cursor cursor=ContactModel.getContactModel().getActivity().getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
        if (cursor!=null){
            while (cursor.moveToNext()){
                Contact contact=new Contact(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)),cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
                ContactModel.getContactModel().getContacts().add(contact);
            }
        }

代码中,ContactModel类和Contact类是我自定义的类,其中获取到的信息,联系人姓名和手机号码都存在了Contact实体类中,在ContactModel中有一个Contact类的List,负责管理获取到的信息。

上面并不是要讲的重点,我们重点来看这句代码:

getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);

在这里我们获取Cursor的实例,正是通过解析这个实例来获得我们想要的信息。

下面我们就分析一下思路:既然是解析的Cursor的实例,那么如果我们拦截到相关的函数,把Cursor的实例返回空值,那么不就是解析不成了么,这样也就禁止了应用偷偷看我们的隐私!

思路有了说干就干,我们可以看到返回Cursor的实例的方法是ContentResolver类的query()方法,我们过去看一下它的源码:

  
    public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable String selection,
            @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return query(uri, projection, selection, selectionArgs, sortOrder, null);
    }

我们很快就发现,这不是它的实现方法,因为在这里它只是单纯的返回了另外一个query方法!

那好,我们接着向下找:

public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable String selection,
            @Nullable String[] selectionArgs, @Nullable String sortOrder,
            @Nullable CancellationSignal cancellationSignal) {
        Bundle queryArgs = createSqlQueryBundle(selection, selectionArgs, sortOrder);
        return query(uri, projection, queryArgs, cancellationSignal);
    }

我们可以从代码看出,这个query()方法和上一个相比,多出一条创建Bundle的语句,然后最后返回的又是一个query()方法!

扫描二维码关注公众号,回复: 2755982 查看本文章

多出的那条创建语句我们暂时不能考虑,这里不是我们的重点,既然这个还不是最终实现方法,那么下一个query()方法估计应该是真正的方法了,我们这就去看一下:

public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable Bundle queryArgs,
            @Nullable CancellationSignal cancellationSignal) {
        Preconditions.checkNotNull(uri, "uri");
        IContentProvider unstableProvider = acquireUnstableProvider(uri);
        if (unstableProvider == null) {
            return null;
        }
        IContentProvider stableProvider = null;
        Cursor qCursor = null;
        try {
            long startTime = SystemClock.uptimeMillis();

            ICancellationSignal remoteCancellationSignal = null;
            if (cancellationSignal != null) {
                cancellationSignal.throwIfCanceled();
                remoteCancellationSignal = unstableProvider.createCancellationSignal();
                cancellationSignal.setRemote(remoteCancellationSignal);
            }
            try {
                qCursor = unstableProvider.query(mPackageName, uri, projection,
                        queryArgs, remoteCancellationSignal);
            } catch (DeadObjectException e) {
                // The remote process has died...  but we only hold an unstable
                // reference though, so we might recover!!!  Let's try!!!!
                // This is exciting!!1!!1!!!!1
                unstableProviderDied(unstableProvider);
                stableProvider = acquireProvider(uri);
                if (stableProvider == null) {
                    return null;
                }
                qCursor = stableProvider.query(
                        mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
            }
            if (qCursor == null) {
                return null;
            }

            // Force query execution.  Might fail and throw a runtime exception here.
            qCursor.getCount();
            long durationMillis = SystemClock.uptimeMillis() - startTime;
            maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);

            // Wrap the cursor object into CursorWrapperInner object.
            final IContentProvider provider = (stableProvider != null) ? stableProvider
                    : acquireProvider(uri);
            final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
            stableProvider = null;
            qCursor = null;
            return wrapper;
        } catch (RemoteException e) {
            // Arbitrary and not worth documenting, as Activity
            // Manager will kill this process shortly anyway.
            return null;
        } finally {
            if (qCursor != null) {
                qCursor.close();
            }
            if (cancellationSignal != null) {
                cancellationSignal.setRemote(null);
            }
            if (unstableProvider != null) {
                releaseUnstableProvider(unstableProvider);
            }
            if (stableProvider != null) {
                releaseProvider(stableProvider);
            }
        }
    }

从代码量上来看,我们终于找到了最终的query()实现方法,赶快去看一下:我们找重点Return返回看起;

第一个return:

IContentProvider unstableProvider = acquireUnstableProvider(uri);
        if (unstableProvider == null) {
            return null;
        }

这段代码位于方法的开始处,我们可以得知query()方法首先通过acquireUnstableProvider()方法获取了一个IContentProvider 类的实例,然后判断是否获取成功,如果获取失败,就返回null空值,也就是返回一个空的Cursor类对象;获取成功就接着进行下面的操作。

第二个return:

try {
                qCursor = unstableProvider.query(mPackageName, uri, projection,
                        queryArgs, remoteCancellationSignal);
            } catch (DeadObjectException e) {
                // The remote process has died...  but we only hold an unstable
                // reference though, so we might recover!!!  Let's try!!!!
                // This is exciting!!1!!1!!!!1
                unstableProviderDied(unstableProvider);
                stableProvider = acquireProvider(uri);
                if (stableProvider == null) {
                    return null;
                }
                qCursor = stableProvider.query(
                        mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
            }

这个地方的return返回也是一个null。代码可以看出,当执行

qCursor = unstableProvider.query(mPackageName, uri, projection,
                        queryArgs, remoteCancellationSignal);

语句报DeadObjectException错误的时候,将会进行一些清理操作,通过acquireProvider()方法获取一个IContentProvider类的对象,然后判断是否获取成功,获取失败同样会返回一个null值。可想而知,在这里获取IContentProvider类的对象大部分应该为null,毕竟这里进行的是错误处理操作,返回null值为正常操作。

第三处return:

if (qCursor == null) {
                return null;
            }

还是返回null!代码很简单,判断了一个对象qCursor是否为空,为空则返回null值。这个qCursor对象在哪里声明的呢,我们还要去看一下:

Cursor qCursor = null;
        try {
            long startTime = SystemClock.uptimeMillis();

            ICancellationSignal remoteCancellationSignal = null;
            if (cancellationSignal != null) {
                cancellationSignal.throwIfCanceled();
                remoteCancellationSignal = unstableProvider.createCancellationSignal();
                cancellationSignal.setRemote(remoteCancellationSignal);
            }
            try {
                qCursor = unstableProvider.query(mPackageName, uri, projection,
                        queryArgs, remoteCancellationSignal);
            } catch (DeadObjectException e) {
                // The remote process has died...  but we only hold an unstable
                // reference though, so we might recover!!!  Let's try!!!!
                // This is exciting!!1!!1!!!!1
                unstableProviderDied(unstableProvider);
                stableProvider = acquireProvider(uri);
                if (stableProvider == null) {
                    return null;
                }
                qCursor = stableProvider.query(
                        mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
            }
            if (qCursor == null) {
                return null;
            }

原来是CurSor类的对象!看一下对它的处理:其中一处try关键字段里,我们提到过了,执行了IContentProvider类的query()方法去赋值,如果出现了DeadObjectException 错误,那么将会重新申请一个IContentProvider类的实例继续执行query()方法来为qCursor赋值。然后如果这两次都赋值失败,那么就会返回null值。

看到这里,我们也明白了,其实真正被调用的是IContentProvider类的query()方法。

接着看第四处return:

qCursor.getCount();
            long durationMillis = SystemClock.uptimeMillis() - startTime;
            maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);

            // Wrap the cursor object into CursorWrapperInner object.
            final IContentProvider provider = (stableProvider != null) ? stableProvider
                    : acquireProvider(uri);
            final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
            stableProvider = null;
            qCursor = null;
            return wrapper;

好了,我们终于看到不是返回一个null值的了!那么被正确返回的就应该是这个wrapper对象了,wrapper是CursorWrapperInner 类的对象,通过调用了CursorWrapperInner 类的构建函数来创建,我们还发现,上面的Cursor对象qCursor,在这里被当作了参数传了进入。这里你可能会说,这里返回的类型是Cursor类,怎么正确返回的是CursorWrapperInner 类呢?

我们可以再细致的看一下,发现CursorWrapperInner 类与Cursor类存在着继承关系。具体的继承关系为:

CursorWrapperInner 继承CrossProcessCursorWrapper继承CursorWrapper继承Cursor。继承了三代。

最后一处return也是返回为null值,位于最后的catch代码块中,没有别的多于代码,这里就不会细致的去分析他了。

好了,分析过query()方法的源码之后,我们下面就开始进行拦截它:毕竟那些偷窥的应用们大多数走了query()方法。我们的目标是,拦截到query()方法后,设置返回值为null,这样应用拿不到Cursor的实例,自然就无法解析我们手机中的联系人信息了~

既然锁定了目标,那就开始我们的拦截:目标类是ContentResolver类,目标方法是query()方法。

上面分析了query()方法后,我们发现这个方法并不是很好拦截,因为它首先参数复杂,可以看一下它的参数:

public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable Bundle queryArgs,
            @Nullable CancellationSignal cancellationSignal)

首先是Uri类型,下面是一个String数组,然后是Bundle类型,最后是CancellationSignal 类型,参数多,参数类型复杂,拦截难度大。还有一个因素,那就是在ContentResolver类中,有三个query()同名方法,这又是一个难度挑战。

我们可以去尝试拦截一下,代码如下:

XposedHelpers.findAndHookMethod(ContentResolver.class, "query", Uri.class,String [].class,Bundle.class,CancellationSignal.class,new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {

                XposedBridge.log("进入函数");
                param.setResult(null);
            }
        });

运行代码,发现拦截失败,报错为找不到该方法。

那怎么办?既然拦截query()方法困难重重,也就意味着拦截query()方法是不明智的,那么我们该如何正确的去拦截呢?

看来只能取巧进行,来一个曲线救国!还记得刚才我给你分析的query()方法的源码吗?我们发现会有四种情况导致query()方法返回为空,这就给我们带来了一个契机~

我们随便挑一个,就拿第一个return  null来讲吧!毕竟query()方法首先执行的就是它,我们回顾一下它的条件:

IContentProvider unstableProvider = acquireUnstableProvider(uri);
        if (unstableProvider == null) {
            return null;
        }

我们发现,这里如果获取IContentProvider 类对象失败就会返回null值,那如果我们拦截这个获取方法acquireUnstableProvider()方法,设置它的返回值为null值,那么也就意味着query()方法也会返回null值!

说干就干,我们去看一下acquireUnstableProvider()方法的源码:

public final IContentProvider acquireUnstableProvider(Uri uri) {
        if (!SCHEME_CONTENT.equals(uri.getScheme())) {
            return null;
        }
        String auth = uri.getAuthority();
        if (auth != null) {
            return acquireUnstableProvider(mContext, uri.getAuthority());
        }
        return null;
    }

看得出这个方法很适合被拦截,首先它的参数构成很简单,就只有一个Uri类,再者它在ContentResolver类中不存在同名方法。无疑,acquireUnstableProvider()方法才是最佳的拦截选择!

现在修改我们的代码:目标类还是ContentResolver类,目标方法是acquireUnstableProvider():

XposedHelpers.findAndHookMethod(ContentResolver.class, "acquireUnstableProvider", Uri.class,new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {

                XposedBridge.log("进入函数");
                
                if(param.args[0]== ContactsContract.CommonDataKinds.Phone.CONTENT_URI){
                    param.setResult(null);
                }
            }
        });

在这里我们可以看到做出了一个if判断处理,我们判断的条件就是请求的参数URI是不是Phone.CONTENT_URI。这里的CONTENT_URI就是Phone类已经封装好的请求联系人的URI,你当然也可以选择自己手写一条URI:content://com.android.contacts/raw_contacts,不过我要告诉你在这里,Phone.CONTENT_URI的值等同于content://com.android.contacts/raw_contacts。不信我们可以去看一下源码:

//这个是Phone.CONTENT_URI
public static final Uri CONTENT_URI = Uri.withAppendedPath(Data.CONTENT_URI,
                    "phones");

//这个是Data.CONTENT_URI
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "data");
 
public static final String AUTHORITY = "com.android.contacts";
public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);

 从上面的源码我们就可以看出来Phone.CONTENT_URI的具体值确实是和自己手写等同的,所以我们在获取query()方法中Uri参数填入Phone.CONTENT_URI没有任何问题。

那么接下来就说一下关于拦截到函数后做出判断Uri的问题。首先我们要知道,在一个应用中,可能会有多处地方需要使用系统的内容提供器,以便获取系统中的一些数据,比如这里我想要获取系统的音乐信息,也是要通过query()方法来获取Cursor类实例然后进行解析获取数据,只不过传入的Uri参数是不一样的。所以如果拦截到方法后不去判断Uri的值,那么导致的后果就是无论传入的什么Uri都无法获取到Cursor实例,无法从内容提供器中获取任何一种数据!所以这里我们就需要判断Uri的值,如果是获取手机联系人信息的Uri,我们才会给结果返回为null值,让它拿不到数据。

运行我们修改后的代码,执行成功!我们成功的令query()方法返回了null值,以后再也不会担心那些令人讨厌的应用偷窥我们的隐私了,因为他们根本就偷窥不到!拦截到方法后就把它的返回值设为null,也就是把IContentProvider 类对象设为null,程序在下一步判断是否获取成功的时候就会执行return  null操作,返回Cursor对象也为空。

最后说一下,开发程序永远不能死劲,思维不要固化,代码是灵活多变的,并不存在一个固定的格式和写法。在平常的开发中,大家要多多的开动脑筋和思维,懂得变通,敢于尝试,这样才能使自己开发出越来越优秀的代码。

本文到此结束,需要引用本文的地方,请标明出处,谢谢!

猜你喜欢

转载自blog.csdn.net/qq_34149335/article/details/81304367