Android逆向工程:大显神通的Xposed,教你动态拦截修改数据的实现

小伙伴们大家好呀,差不多一周未见啦~在上篇的博客中我们主要学习了利用Xposed的报错机制来快速洞晓程序的执行过程,学下来是不是感觉打开了一扇新世界呢!那么在今天的这篇博文中,我们将继续学习Xposed相关的惊人操作,快点跟上步伐和我一块来学习吧~

正如标题所见,今天我们学习的内容为如何动态拦截修改数据。关于拦截修改你可能会说,这些都非常的常见啊,没什么稀奇的啊,Xposed使用不就是拦截数据然后修改的嘛!那么我可就要说啦,我把拦截数据修改分为两个部分,一个是静态修改,一个是动态修改。所谓的静态修改就是Xposed很常见的操作,只要抓住这个方法,那么就把这个方法的执行结果修改为某一值,或者把这个方法的参数修改为某一值。

我们发现,这种静态的修改永远只会修改成一个固定的值,下面我们就想一下,程序的运行是动态的瞬息万变的,可能某一个方法在不同的运行时刻会返回不同的数据结果,打个比方,函数a()在第一秒的时候需要返回为1,在第二秒的时候需要返回2,在第三秒的时候需要返回3,,,我们使用静态拦截修改,在第一秒的时候我们拦截到它修改返回结果为1,第二秒我们还是修改结果为1,第三秒修改返回结果又是1,,,那么你告诉我这个时候程序会怎么运行?所以这就是为什么有时候我们拦截到某一方法却导致程序运行崩溃或者运行出错的问题!

面对上述返回多结果的方法,我们就需要实现动态修改,具体某一时刻要返回什么样的结果,具体根据程序的执行情况来判断。动态修改就是修改的结果是不会固定的。解释了这么多,估计你也明白了,那么就让我们开始今天的学习吧:

首先还是有请我们今天的教案对象,腾讯旗下的产品:应用宝,本次的讲解学习还是以应用宝案例,博主这里使用的应用宝版本为:7.2.6。感谢应用宝的辛苦付出以身施教,还是要强调一下哦,注重提高技术,搞破坏还是不允许的!

就让我们赶快开始吧!在上篇的博文中,我们具体分析了应用宝的源码,主要是提交评论信息模块。我们通过对应用宝的逆向分析,我们惊喜的发现在应用宝中访问网络有一个基本的父类,这样使得我们可以轻松借助报错机制来快速获知程序进行网络请求的相关信息。那么今天我们需要实现的是,把它访问网络上传的数据值给改掉!

修改上传数据这个很显然不能来一个静态修改,毕竟程序每时每刻上传数据的格式,类型,数值都不是固定的,所以这里只能使用动态修改来实现了。本次动态修改我们主要拿应用宝评论数据上传类CommentAppEngine开刀。首先讲解动态修改的第一部分,函数联动动态修改!

你可能又要懵逼了,函数联动动态修改???喵喵喵?这是什么鬼,听都没听说过啊!别急,这个也是博主自己划分的命名,下面就给你解释什么是函数联动动态修改!首先看代码:

上面的代码模块就是提交评论类的相关代码。在这里我首先先夸奖一下腾讯的安全策略,做的是不愧是天衣无缝啊!为什么?且看上面的代码,让我给你分析分析。起初博主想的是拦截send()方法,替换他的第一个参数数据,即保存上传数据的实体类CommentAppRequest,这边给他另外造一个新的CommentAppRequest类示例,里面都是假数据,然后交给程序进行上传不就好了嘛~听上去很简单,但是当博主实际去做的时候发现,远远没有我想得这么简单!当我千辛万苦组装了一个新的 CommentAppRequest实例,填充运行的时候却给我报错!提醒我参数类型错误.......

为什么会这样?参数类型怎么会错呢!博主仔细看了以下代码,这才恍然大悟,原来第一个参数类型传递的是JceStruct类,而不是CommentAppRequest类!那么JceStruct类又是什么鬼?看下源代码:

这是 JceStruct类的部分源码,我们很容易的就会发现这货是一个抽象类。。。看下那个封装了上传评论数据的CommentAppRequest类,果不其然,CommentAppRequest类继承了JceStruct类,那么也就就可以很好的解释下面这句代码: JceStruct commentAppRequest = new CommentAppRequest();,事实上你会发现,在应用宝中几乎所有的封装上传数据的类都继承于JceStruct类。。。

很好这下问题就很棘手了,使用了抽象类来声明一个子类并进行相关的赋值操作,不得不说这招确实是很高明!安全性蹭蹭的往上飙!因为你根本就没有办法在脚本中去做抽象类调用它的子类操作,,,没法去模拟。所以这里就很值得我们借鉴去学习,在我们平时的开发活动中,利用这种架构会使得我们的软件安全性得到很大的提高,侧面也就说明了,一个好的架构对于一款应用来说是多么的重要!这下明白了开发模式的重要性了吧!

说了这么多,那么到底能不能把它干掉啊?!如此安全架构就没有什么可以突破的地方吗?答案肯定不,世界上没有完美的代码,关键就看你的破解思路是多么的巧妙了!

既然这个send()方法拦截不成,那么我们就要转移目标,我们发现,这个send()方法在方法a()中的最后进行的调用,a()方法中就是进行数据组装的操作。那么我们可不可以把a()方法拦截掉,修改传入的参数数据,这样进入a()方法内它组装的数据也就会是假数据,也是达到了修改的目的。说干就干,我们具体看一下a()方法:

  public int a(CommentDetail commentDetail, long j, long j2, String str, String str2, String str3) {
        JceStruct commentAppRequest = new CommentAppRequest();
        commentAppRequest.a = j;
        commentAppRequest.b = j2;
        commentAppRequest.j = str2;
        commentAppRequest.k = str3;
        commentAppRequest.c = commentDetail.b;
        commentAppRequest.d = h.a().t();
        commentAppRequest.e = commentDetail.c;
        commentAppRequest.f = commentDetail.d;
        commentAppRequest.g = commentDetail.g;
        commentAppRequest.h = str;
        commentAppRequest.i = commentDetail.i;
        XLog.d("comment", "CommentAppEngine.sendRequest, CommentAppRequest=" + commentAppRequest.toString());
        return a(commentAppRequest, (byte) 1, ProtocolContanst.PROTOCOL_FUNCID_COMMENT_APP);
    }

上面就是a()方法的具体逻辑,我们发现传入的参数确实是为CommentAppRequest类进行了组装,所以拦截他完全是可行的,不过还有一个地方,,,,在a()方法内,我们发现了这句代码:commentAppRequest.d = h.a().t();,我们发现commentAppRequest类中的变量d并不是由传入参数进行赋值的,而是调用了一个另外的返回方法t()来赋值,那么这个t()方法返回的是什么什么数据?commentAppRequest类中的变量d到底有什么意义呢?

很简单,我们通过打印参数信息就可以获取具体的上传数据,然后分析数据含义:

这是博主打印出的上传数据信息,分析参数d很快就能得出d为QQ账号,QQ账号还是一个很重要的判断依据,毕竟大概这个世界上不会存在两个相同的QQ账号。那么也就是说h.a().t();这个方法返回的就是已登录账号的QQ账号。这里又是一个值得夸赞的地方,我们可以发现QQ账号并没有作为参数传进来,这样就保证了篡改QQ账号的难度,毕竟这个属于一个联动调用,在调用方法a()的同时会调用t()方法来获取QQ账号,那么如果要对上传评论数据进行修改就需要同时兼顾这两个地方,保证QQ账号返回并不是固定,而且是符合里面的头像地址对应规则!

这样的话就需要函数联动动态修改了,函数联动动态修改主要应用于上面这种出现联动调用,触发调用的一个函数体内多函数调用的情况。那么怎么去实现这个联动调用呢?下面就开始我们的实现逻辑:

解决思路:脚本内声明一个变量用来保存需要修改的数据,然后在抓到某函数后,对该变量进行赋值操作,在另外那个需要拦截的函数那里,设置它返回结果为该变量的值,这样实现了一个联动动态修改。

是不是听着有些绕??也是,只不过往常我们进行Hook操作,通常只针对一个方法的逻辑处理,而这次需要同时处理两个方法,关键是这两个方法之间有一个变量作为纽带联系。

说白了也就是,脚本内部声明一个变量m,这个变量m会在方法a()被拦截到后,根据具体的数据被赋某一值,然后在t()方法内设置返回结果为m,当然需要判断m的数据情况,如果发现未被赋值,那么还是返回原返回值!就是这么个过程,下面就让我们开始上手代码吧:

private void setHookComment(final XC_LoadPackage.LoadPackageParam loadPackageParam){
        final Class commentDetail=XposedHelpers.findClass("com.tencent.assistant.protocol.jce.CommentDetail",loadPackageParam.classLoader);
        XposedHelpers.findAndHookMethod("com.tencent.nucleus.socialcontact.comment.CommentAppEngine", loadPackageParam.classLoader, "a", commentDetail, long.class, long.class, String.class, String.class, String.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

             
                    if (userInfo.size() > 0) {
                        int index = indexo.size();
                        String s = userInfo.get(index);
                        String[] str = s.split("--");
                     

                        isStart=true;
                        qq = Long.parseLong(str[2]);
                        indexo.add(index);
                    }

                
            }
        });
    }

 以上是博主的代码,为了安全起见,已经删去部分逻辑代码。那么我们就开始分析代码的实现:

首先声明两个变量,一个long型的变量,变量名为qq,起始值为0,另外一个是Boolean型的变量,名字为isStart,起始值为false。从代码中我们就很轻易地看出,首先我们拦截CommentAppEngine类中的a()方法,进入拦截方法后,我们开始读取一个文件里面的内容,里面存放的是对应的QQ账号。首先先判断是否读取成功,读取成功后拿到一条字符串,进行字符串分割生成一个数组,数组的最后一个下标就是所用到的QQ账号。拿到本次需要返回的QQ账号后,给我们的变量qq进行赋值,同时修改标志isStart为true。这样拦截到a()方法后进行的操作就结束了,总共两部分,修改变量值qq,修改判断标志isStart。

那么下面就是拦截t()方法后的操作了,代码为:

private void setHookQQNumber(final XC_LoadPackage.LoadPackageParam loadPackageParam){
        XposedHelpers.findAndHookMethod("com.tencent.nucleus.socialcontact.login.h", loadPackageParam.classLoader, "t", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                if (isStart){
                    XposedBridge.log("开始设置QQ账号为:"+qq);
                    param.setResult(qq);
                    isStart=false;
                }
            }
        });
    }

逻辑也很简单清晰,拦到方法后开始判断标志,看看是否允许修改返回结果,如果isStart为true,那么也就意味着要修改返回结果,所以就直接设置结果为已经被赋值的变量qq,同时判断标志isStart再次修改为false!这一步很是关键,这样就保证了只有这一次是返回了假的qq账号,在别的时刻还是返回为真实的QQ账号,避免了在程序中露出马脚!

这里还有需要关注的点,那就是方法的抓取时机。对于a()方法,一定要是重写的是beforeHookedMethod(),而不是afterHookedMethod()。我知道很多人都有对这两个方法的使用有比较的误解,比如就认为这两个方法没啥两样,使用哪一个都无所谓~这样是很不对的,在博主看来,这两个方法使用的差别很大,具体使用哪一个要根据你的业务逻辑来确定,切不可胡乱使用!

beforeHookedMethod()方法会在被Hook的方法被调用之前把该方法拦截下来,此时的情况是该方法正要调用但还没有调用的状态,所以使用beforeHookedMethod()可以用来修改传入参数数据相关,进行联动动态修改。afterHookedMethod()方法会在被Hook的方法已经执行完毕后把该方法拦截下来,此时的情况是该方法已经执行完毕,正要等待返回,所以使用afterHookedMethod()可以用来修改返回结果。在afterHookedMethod()中修改参数数据是没有任何效果的!

一般情况下,进行动态拦截修改数据,主要是在beforeHookedMethod()中比较多。

说完了函数联动动态修改,那么下面是比较简单的单一动态修改了,只需要修改某一项参数数据即可,还是那个a()方法,我们注意到它的第一个参数类型为CommentDetail类,我们从名字就可以看出这里面也是封装了好多的评论数据,看下a()方法的逻辑代码,我们发现确实如此,所以要想实现修改掉上传评论数据,我们还需要把这个CommentDetail类的参数给干掉!

首先去看一下CommentDetail类的代码:

我去这么多的变量,好可怕!难道这么多变量都要改掉吗?不不不,我们只需要改它需要的变量!

对比a()方法的代码,我们发现对于 CommentDetail类只是使用了下面几个变量的值:b,c,d,g,i。总共五个,改起来还是比较轻松的!那么就让我们开始代码实现吧:

private void setHookComment(final XC_LoadPackage.LoadPackageParam loadPackageParam){
        final Class commentDetail=XposedHelpers.findClass("com.tencent.assistant.protocol.jce.CommentDetail",loadPackageParam.classLoader);
        XposedHelpers.findAndHookMethod("com.tencent.nucleus.socialcontact.comment.CommentAppEngine", loadPackageParam.classLoader, "a", commentDetail, long.class, long.class, String.class, String.class, String.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("抓住方法CommentAppEngine->a(CommentDetail,long,long,String,String,String)");
                Object object=commentDetail.newInstance();

                if (userInfo.size()>0&&sts.size()>0&&comments.size()>0) {

                    if (userInfo.size() > 0) {
                        int index = indexo.size();
                        String s = userInfo.get(index);
                        String[] str = s.split("--");
                        commentDetail.getField("c").set(object, str[1]);
                        commentDetail.getField("i").set(object, str[0]);
                        commentDetail.getField("b").set(object, 5);

                        isStart=true;
                        qq = Long.parseLong(str[2]);
                        indexo.add(index);
                    }

                    if (sts.size() > 0) {

                        for (String s : sts) {
                            String[] str = s.split(":");
                            if (str[0].equals("f")) {
                                commentDetail.getField("d").set(object, Integer.valueOf(str[1]));
                            }
                           
                        }
                    }

                    if (comments.size() > 0) {
                        String s = comments.get(0);
                        commentDetail.getField("g").set(object, s);
                    
                    }

                    XposedBridge.log("开始修改参数信息");
                    param.args[0] = object;
                    

   
                }
            }
        });
    }

为了安全起见,以上代码已经被删减一部分,删减部分和本次教案无关。那么接下来就讲解下这段代码:有小伙伴已经看出来了,又是Java反射机制!对的没错,Java反射机制和Xposed进行碰撞能够激发出你无法想象的能力!

首先我们调用findClass()静态方法来抓取这个自定义的 CommentDetail类。下面就开始使用Java的反射机制,调用newInstance()方法生成一个Object对象,然后调用set()方法对某一变量进行赋值操作,赋值结束后,代码param.args[0] = object,把object对象赋值给第一个参数,这样即完成了修改数据操作!

下面就让我们运行以下看看结果如果,是否实现了我们的函数联动动态修改和单一动态修改:

我们通过抓取查看上传 的评论数据,发现我们函数联动动态修改的QQ账号确实被修改掉了,而且我们单一动态修改的数据也是实现的!看来这次还是挺不错的呦~

好啦今天的学习就到此结束了,通过今天的学习我们明白了什么是动态拦截修改,以及两种实现动态拦截修改的方法策略。不知道你是否已经掌握了呢!还需要多多练习,努力加油才行,后面还会有更多的Xposed精彩操作讲解呦!

本篇博文到此结束,有不懂的地方请评论留言告诉我,我会一一做出解答!有需要引用本文的地方,请标明出处,谢谢合作!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值