Android逆向工程:大显神通的Xposed,利用报错机制快速获取程序运行过程

好久不见,已经一个多月没有更新博客了,博主一直在忙于新的破解项目。在之前的博客中,我们学习了该如何去定位关键代码段,该如何去修改smali代码,以及对各种逆向工具的使用。那么在接下来的博客中,我们将会具体学习到Xposed在逆向工程中的使用和开发技巧。

关于Xposed,在博主最早的那几篇博客中已经有过介绍,以及给大家分享了使用Xposed拦截修改各种手机参数的知识。修改手机参数其实对Xposed来说是小菜一碟,真正能够让它大显神通的地方,还是我们的逆向工程。

那么Xposed在逆向工程中到底启到多大的作用?接下来你就会很快的理解到!在此说明一下,在明白了Xposed的威力后,大家还是不要放弃Smali代码的学习,毕竟Smali才是安卓逆向中最重要的知识,可不能贪恋Xposed而放松了Smali,这样对逆向技术的提高会造成很大的阻碍。个人建议是,Xposed在逆向工程中启到的辅助作用,进行相关的数据参数打印以及关键判断反法拦截设置即可,修改Smali代码对应用进行完全破解,还是最重要的。

正如我们的标题所示,今天我们会学习到使用Xposed的报错机制,进行快速获取程序执行动态,这可谓是逆向工程中的最强辅助!这里你可能会很好奇,报错机制?我们在程序的开发过程中,各种各样的报错我们屡见不鲜,所谓报错也就是程序中出现Bug导致程序崩溃进行打印出的一系列错误信息,我们经常通过错误信息来进行相关Bug的修改。那么在逆向工程中,错误信息通过巧妙利用,摇身一变就会成为程序进程通风报信的奸细,让逆向工程师很容易会获知程序一路发生了什么,调用了什么方法和类!正如你所示,Xposed大显神通从现在开始。

首先有请我们今天的教案对象,腾讯旗下的成品:应用宝。本次博客的技术讲解将会以应用宝为实验对象,博主这里使用的应用宝版本为:7.2.6。本次讲解感谢应用宝的辛苦付出,以及还要说一下哦,只学技术,搞破坏是不可以的!

应用宝是腾讯旗下的应用市场。腾讯作为国内的知名大厂,他们的安全团队实力不容小觑,在国际竞赛中,博主记得和360的安全团队不分上下的存在。应用宝背后的安全策略也是复杂异常,腾讯果然是非常注重产品的安全,使用常规手段去破解应用宝是难上加难。起先,博主尝试对应用宝进行了反编译操作,在二次打包的签名却最终失败,仔细看了一下错误信息,发现应用宝使用的热更新的相关技术,里面有特殊资源是经过网络进行动态加载上,导致在二次打包环节报资源丢失错误,二次改造估计是不行的了,于是博主只能把目光转向了使用Xposed进行相关破解工作。这里值的一提的是,应用宝并没有进行相关加壳,源码可以窥探到,可能腾讯非常自信和信赖它的安全技术,确实,他们安全做的超级棒!然而世界上并没有完美的代码,漏洞找找总会有的。博主在最后会对应用宝的安全策略进行点评,以及应用宝现阶段存在的安全漏洞,同时分享一些安全开发知识。

讲解从获取应用宝的上传数据开始。你可能会说获取上传数据有什么用,都是加密的信息,获取到了也是白搭。这里博主想说,加密?作为一名逆向工程师,什么加密都视为不存在的,因为我们在信息被加密之前就已经拿到手了!

这里我们的目标是获取到应用宝上传评论数据的信息。就此开始!

首先还是快速定位,进行页面元素解析:

这是评论页面,我们重点审查“提交”按钮:

通过解析元素我们可以知道,这里的“提交”按钮是一个TextView,Id值为:yc。看到这个yc就应该明白的到,这里的Id也是被混淆了,不得不表扬一下腾讯的安全策略,考虑的是方方面面。不过这个yc还是没问题的,我们拿到AndroidStudio中,对整个项目进行全局搜索:如图所示

好了嘛,一下子出现在这么多,我们想象中的内存地址并没有出现!想想也是,毕竟我们是对“yc” 这个字符串进行搜索,不存在特殊字符,任何一段代码中都可能包含yc这个字符串,这无疑是为全局搜索增加了很大的难度。这里就看出腾讯的良苦用心了,ID值给你进行混淆,会使得你在全局搜索中比较难以找到该ID下的内存地址,不得不说,这招真的很漂亮,没毛病!

那么这该怎么办?针对这种一出来好大对的,博主不再推荐使用这种全局搜索,转而直接去public.xml和R.java这两个文件中进行局部搜索!毕竟整个项目中所有的ID定义都是在这两个文件中。那么我们就去public.xml文件中局部搜索“yc”:

哈哈,马上就找到了,内存地址为:0x7f0d0802。这里还要注意以下,字段“type”的值一定要是“id”,因为我们毕竟找的是一个ID值,而不是别的。知道了内存地址,下面就好办了,对内存地址进行全局搜索:

 emmm........这下又懵逼了,又一下子搜出这么多引用到的地方!这该怎么办?使用我们选多不选少的原则进行审视,发现都差不多啊,这就很棘手了。那么接下来就需要这么做,我们分析“提交”按钮的功能,很明显点击提交就会把评论内容和评分给提交到服务器,那么也就是说这里会有一个点击事件监听以及事件实现方法,回想我们开发过程中如何实现点击事件方法?重写onClick()方法,方法内是一个switch多分支选择,case R.id.XXX,.,,,,,,,如此一来,结合我们的smali语法中的switch方式,我们会轻易发现,确实有实现点击方法的语句:0x7f0d0802 -> :sswitch_0,那么我们直接就可以去看看针对控件“yc”的点击事件实现方法是什么,共有三处,还好工作量不是很大!

下面就使用jadx-gui来获取应用宝的源码,当然是已经混淆过的,还好没有加壳,加壳的话就更难搞了。很多人觉得逆向工程难以理解,难以学习掌握,原因是一方面Smali代码难以理解,再者就是这个混淆后的代码让人看得头大!确实,一堆莫名其妙的abcdefgh,,,,看着就会头疼!没有混淆的代码都会看不下去,更何况这一堆的abcd!大家都喜欢写代码,不喜欢看代码,这样下去其实很不利于自己的技术提高,经常回头看看自己写过的代码,你就会发现你的不足之处在哪里,放置自己的不足不管,无疑是对自己的扼杀。

好了,就让我们去看看这个混淆后的代码吧,首先是em类中的点击方法:

比较短,吸引我们注意的是代码this.a.n.a(xxx),我们点进去看一下这个a()方法实现:

 

果不其然,一个非常明显的网络请求类中的方法,继承有一个父类名称为BaseEngine,然后重写了父类中的请求成功和请求失败的两个方法,这里我们可以从类名中得知该请求类的功能:提交App报告的相关数据信息。该请求类并不是我们需要找的那个提交评论请求类,所以这个就抛弃,我们看下一个:bu类中点击事件的实现方法:

由于该实现方法比较长,这里不再用图片展示,使用代码的方式:

 case R.id.yc:
                this.a.d();
                round = Math.round(this.a.b.getRating());
                if (round <= 0) {
                    this.a.e.setText(R.string.gp);
                    this.a.e.setTextColor(-65536);
                    return;
                }
                Object obj = this.a.c.getText().toString();
                if (!TextUtils.isEmpty(obj)) {
                    if (obj.length() > 100) {
                        this.a.f.setVisibility(0);
                        this.a.f.setText(R.string.h_);
                        return;
                    }
                    obj = null;
                    CharSequence text = this.a.f.getText();
                    if (text != null) {
                        obj = text.toString();
                    }
                    string = this.a.a.getString(R.string.h_);
                    if (this.a.f.getVisibility() == 0 && !TextUtils.isEmpty(r0) && r0.equals(string)) {
                        this.a.f.setVisibility(8);
                    }
                }
                if (this.a.y.n()) {
                    String str2;
                    if (TextUtils.isEmpty(this.a.c.getText().toString())) {
                        this.a.k.g = "";
                        this.a.c.setText("");
                    } else {
                        this.a.k.g = this.a.c.getText().toString().replaceAll("\n", "\t");
                    }
                    this.a.k.a = new Date().getTime() / 1000;
                    this.a.k.c = this.a.y.u();
                    this.a.k.b = round;
                    str = "";
                    if (!ai.a().d(this.a.E) || !ai.a().e(this.a.E)) {
                        LocalApkInfo localApkInfo = ApkResourceManager.getInstance().getLocalApkInfo(this.a.E);
                        if (localApkInfo != null) {
                            i = localApkInfo.mVersionCode;
                            str2 = localApkInfo.mVersionName;
                        } else {
                            return;
                        }
                    } else if (ai.a().a(this.a.E) != null) {
                        i = this.a.r;
                        str2 = this.a.s;
                    } else {
                        return;
                    }
                    if (i > 0) {
                        this.a.k.d = i;
                        this.a.k.i = LoginUtils.f().iconUrl;
                        if (this.a.j != null) {
                            this.a.j.a(this.a.c.getText().toString(), round, i, this.a.A);
                        }
                        if (this.a.l != null) {
                            if (i == this.a.r) {
                                this.a.l.a(this.a.k, this.a.o, this.a.p, str2, this.a.q, this.a.E);
                            } else {
                                this.a.l.a(this.a.k, this.a.o, -1, str2, this.a.q, this.a.E);
                            }
                        }
                        if ((this.a.z || this.a.A) && this.a.A && this.a.B != null) {
                            if (this.a.z) {
                                this.a.B.b = this.a.c.getText().toString();
                                this.a.C.a((Activity) this.a.a, this.a.B);
                            } else if (this.a.A && this.a.y.z()) {
                                this.a.B.m = this.a.c.getText().toString();
                                this.a.C.a(this.a.a, this.a.B, true, true);
                            }
                        }
                        this.a.dismiss();
                        return;
                    }
                    return;
                }
                this.a.h();
                return;

我们可以看出这个bu类中的实现方式真的很长,那么他有没有可能是我们需要寻找的类呢?那就需要细致地进行逻辑分析了,最终我们确定一个关键方法为this.a.l.a(),我们点进去看a()方法的具体实现:

 又是一个请求类,看下类名:评论App,没差了,就是这个请求类向服务器提交了我们的评论数据。分析他的特点,同样继承自BaseEngine,里面同样是重现了成功和失败两种方法,调用都是父类BaseEngine的请求方法。这里我们就可以猜想一下,BaseEngine请求类是应用宝代码中全体请求类的父类, 应用宝进行网络请求统一走的是BaseEngine中的请求方法,所以我们这里很有必要去看下BaseEngine这个类!

没有太多的东西,很明显的是BaseEngine类是一个抽象类,里面方法是线程开启,界面更新这些,继承自父类BaseModuleEngine,说明BaseEngine只是对BaseModuleEngine类做出的进一步的封装,真正请求网络的实现方法在BaseModuleEngine类中!我们主要看请求方法a(xxx,(byte) 1,xxx),点进去查看:

确实进入了 BaseModuleEngine类中,但是方法名好像不对,明明我们方法名是a,怎么跳转到send()方法呢?为了保险起见,我们去看以下Smali代码中是如何跳转的:

Smali代码:

invoke-virtual {p0, v0, v1, v2}, Lcom/tencent/pangu/module/AppReportEngine;->send(Lcom/qq/taf/jce/JceStruct;BLjava/lang/String;)I

    move-result v0

    return v0

从代码上就可以看出,确实是调用的为send()方法,这也难怪为什么我们点进去会为我们跳到send()方法。出现这种状况不好判断,但是逆向中有一个唯一标准:一切都以Smali代码为准!Smali代码中怎么写程序百分之百会怎么执行!

好了,到这里我们已经找出了提交数据的请求基类BaseModuleEngine,和请求方法send()方法,讲了这么多,你可能会说Xposed怎么还不出场!本片博客的主题难道不是讲Xposed的吗?!哈哈,别急,Xposed马上就登场!毕竟使用Xposed要明确的是目标类和目标方法,前面我们做了这么多的方法,就是为了找到目标类和目标方法!

下面就是Xposed大显神通的时刻!我们将会利用报错机制来完美展现程序的运行流程,重点就要来了!

前面我们已经做出了猜测,BaseModuleEngine类可能为应用宝代码中执行网络请求的一个基类,这样的话就可以利用报错机制来获取某一时刻是谁进行了网络请求!

划重点:只有满足多个子类执行同一个基类中的方法的时候,才能使用报错机制

 进入Xposed的时刻,讲解该如何写报错机制!

xposed代码如下,目标类是BaseModuleEngine,目标方法是send:

private void setHookSend(final XC_LoadPackage.LoadPackageParam loadPackageParam){
        final Class jceStruct= XposedHelpers.findClass("com.qq.taf.jce.JceStruct",loadPackageParam.classLoader);

        XposedHelpers.findAndHookMethod("com.tencent.assistant.module.BaseModuleEngine", loadPackageParam.classLoader, "send", jceStruct, byte.class, String.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("抓到函数:BaseModuleEngine->send(JceStruct,byte,String)");

               
                Class commentAppRequest=XposedHelpers.findClass("com.tencent.assistant.protocol.jce.CommentAppRequest",loadPackageParam.classLoader);
                Field[] fs = commentAppRequest.getDeclaredFields();
                for (Field field : fs) {
                    XposedBridge.log("参数"+field.getName()+"值为:"+field.get(param.args[0]));
                    
                }

               
        });
    }

这里眼尖的小伙伴肯定发现了:Java反射机制!没错,这里我们会使用到Java反射,目的是让它报错从而获取到是谁调用了网络请求!这里的JceStruct类是应用宝中自定义的类,因为这里它需要作为一个参数,所以我们需要拿到这个类,使用XposedHelpers.findClass()方法获取!

进入到拦截函数后,我们打印上传的数据,这里通过Java反射去调用访问变量值然后进行打印。这里打印CommentAppRequest类的变量值。如果发现此时的类不是CommentAppRequest类,那么就会进行报错,会告诉你此时的类是谁。就是通过这种报错机制才使得我们窥探程序运行状态有了可能,下面我们就运行Xposed看一下:

 报错信息为:期望是CommentAppRequest类,但是拿到的却是StatReportRequest类,我们就会清楚这个时刻是StatReportRequest对应的那个请求类进行了请求!具体的类路径也很详细

 经过这个你就会发现,原来报错信息还可以这样使用!真的是很方便很快捷的获取有没有!真的是谁进行了网络请求一目了然。整个运行状态随时掌握。

这样目的就是会大大降低逆向的时间成本,可以快速而准确的切中要害。Xposed在逆向中的作用使它成为逆向工程师的最佳辅助!后面的博客中,博主会继续带大家领略Xposed的强大之处,各种特殊的技巧,大家还要记得好好学习昂!

本篇博文到此结束,如有引用请标明出处!有不同的请评论留言,我会一一进行解答!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值