Bu弃 发表于 2018-8-15 17:20:36

“某开分身”永久VIP Xposed实现及分析

本帖最后由 Bu弃 于 2018-8-16 13:36 编辑

Hello Everyone~ 在论坛看到Rooking表哥发的“多开分身最新版7.2永久VIP版”(https://www.chinapyg.com/thread-120623-1-1.html)一时手痒,自己也抽空分析了下,使用Xposed实现永久VIP。下面是分析全过程~~ 另:特别感谢周年群中的大表哥们提供脱壳后的dex文件。
一、观察
在着手分析APK前,我们先装上app,观察它的一些特征。

在“个人中心”页面,有显示是否为会员,以及到期时间。如果我们第一次运行的时候是断网的,会提示请连接网络什么的。在这里,我们可以确定一件事:app在打开时会请求服务器,获取到期时间以及是否为会员,还有广告等等。
二、分析
   上面说到了,应用会联网获取到期时间等信息,那么我们可以通过抓包的方式,来确定app发送的请求。然后通过该url进行全局搜索。更加容易定位到关键处。这里我使用Fiddler来抓包。
Fiddler配置我就不说了,百度一大堆。抓包结果如下:

为什么确定是这些?因为该app的官网就是91xxx.cn.接下来一个个看,于是发现该连接最可疑..

Jadx打开脱壳后的dex(由于某些原因,这里就不传dex了),Navigation->Text Search

搜索链接“/ServerV60?fn=it”结果有很多。如下

我们点开第一个观察先。代码如下:

private void a() {
      this.g = true;
      try {
            String str;
            CoreEntity a = new a(this).a();
            Object obj = "测试";
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("maxCore = ");
            stringBuilder.append(a == null ? 0 : a.getCode());
            j.a(obj, stringBuilder.toString());
            GetBuilder getBuilder = (GetBuilder) OkHttpUtils.get().url("http://chaos.91ishare.cn/ServerV60?fn=it"); //这就是请求的连接
            String str2 = "o";
            if (a == null) {
                str = "0";
            } else {
                StringBuilder stringBuilder2 = new StringBuilder();
                stringBuilder2.append(a.getCode());
                stringBuilder2.append("");
                str = stringBuilder2.toString();
            }
            getBuilder.addParams(str2, str).build().execute(new b(this) { //拼接参数并执行请求
                final /* synthetic */ Splash a;

                {
                  this.a = r1;
                }

                public /* synthetic */ void onResponse(Object obj, int i) { //服务器响应后回调,传回一个Json
                  a((JSONObject) obj, i);
                }

                public void onError(Call call, Exception exception, int i) {//连接失败时回调
                  u.a(this.a, "网络连接失败");
                  this.a.g = false;
                  this.a.c.setVisibility(8);
                  this.a.ll_no_networks.setVisibility(0);
                }

                public void a(JSONObject jSONObject, int i) {//如果服务器有响应,就会在onResponse中执行改方法
                  if (jSONObject == null) {
                        return;
                  }
                  if (com.bly.dkplat.a.a.a().a(jSONObject) != null) {
                        this.a.b();
                        return;
                  }
                  this.a.c.setVisibility(8);
                  u.a(this.a, "网络连接失败");
                  this.a.g = false;
                  this.a.ll_no_networks.setVisibility(0);
                }
            });
      } catch (Exception e) {
            e.printStackTrace();
            u.a(this, "初始化失败");
            this.g = false;
            this.ll_no_networks.setVisibility(0);
      }
    }


我们点开com.bly.dkplat.a.a.a().a(jSONObject)这个方法进去看看。很遗憾,jadx没有反编译出这个方法体。所以我们只能打开JEB,找到这个类。代码如下:

public boolean a(JSONObject arg9) {
      __monitor_enter(this);
      try {
            j.a("initCacheByApiResult", arg9);
            if(!StringUtils.isBlank(i.a(arg9, "err"))) {
                goto label_127;
            }
            //看到这里就很熟悉了,这就是抓包抓到的返回,arg9就是传递过来的json。i.a()就是读取该json中的对应的数据
            a.a().a(i.a(arg9, "et", 0));   //这里是读取过期时间,为什么这么说呢,可以去看抓到的内容,et的值是一个时间戳,转换下就是到期时间。可以自己去尝试下
            a.a().a(i.a(arg9, "iv", 0));//这个就是是否为vip
            a v0 = a.a();
            boolean v2 = i.a(arg9, "sa", 0) == 1 ? true : false;
            v0.a(v2);
            Log.e("广告测试", "sa = " + a.a().c());
            boolean v0_1 = a.a().c();
            String v2_2 = Application.IMEI;
            boolean v4 = a.a().g() == 1 ? true : false;
            com.bly.dkplat.utils.b.i.a(v0_1, v2_2, v4, a.a().f());
            a.a().a(i.a(arg9, "m"));
            a.a().b(i.a(arg9, "kf"));
            a.a().b(i.a(arg9, "qt", 0));
            a.a().c(i.a(arg9, "yz"));
            a.a().d(i.a(arg9, "au"));
            a.a().f(i.a(arg9, "adp"));
            h.a(i.e(arg9, "os"));
            a.a().c(i.a(arg9, "fk", 0));
            v0 = a.a();
            v2 = i.b(arg9, "fc") > 0 ? true : false;
            v0.c(v2);
            a.a().d(i.a(arg9, "ud", -1));
            a.a().e(i.a(arg9, "wk", 0));
            String v9_1 = i.a(arg9, "rgps");
            if(StringUtils.isNotBlank(v9_1)) {
                a.a().e(v9_1);
            }

            this.t();
            a.a().b(true);
            if(this.j != 1) {
                long v6 = this.j - 86400000;
                if(System.currentTimeMillis() < v6) {
                  com.bly.dkplat.utils.a.a(Application.getInstance(), v6);
                }
                else {
                  com.bly.dkplat.utils.a.a(Application.getInstance());
                }
            }
            else {
                com.bly.dkplat.utils.a.a(Application.getInstance());
            }

            if(!this.d()) {
                d.a();
                c.c();
            }
      }
      catch(Throwable v9) {
            __monitor_exit(this);
            throw v9;
      }

      __monitor_exit(this);
      return 1;
    label_127:
      __monitor_exit(this);
      return 0;
    }



其他的我们就不分析了,就把vip和到期时间无限制先实现。其余的有空再慢慢研究~~。我们可以看到调用a.a().a(),把从json读取的值传过去。我们点进去看看。代码如下:

public void a(String arg1) {
      this.l = arg1;//赋值给l。
}


从这大概可以看出来了大致流程了。即,从服务器读取数据,然后解析json,给类中的属性赋值。而后就是通过该类中的属性进行判断和显示。
到这,我们已经有了Xposed的思路了。只要Hook了给类中属性赋值的方法就可以达到目的了。
哦,对了,还有个东西忘了说了。既然我们知道只要Hook了属性赋值的地方就可以了,那么这个值是多少合适?既然我们已经知道过期时间是通过值“l”判断的,那么我们看看有什么地方调用了它。在JEB中选中该属性,右键->Cross references 查看调用

这里获取值,按道理应该是调用get方法,所以该方法应该没有参数。于是锁定了最后一个f()方法。同样,选中该方法,右键->Cross references 查看调用.然后这里又有个问题,我的JEB是没有反编译该方法的。所以只能在jadx中看了。在jadx中找到f()方法,选中,右键->Find Usage

我们发现第三个有点可疑,点过去看下。代码如下:

public void f() {
      k.a(getTag(), "UserFragment initDatas run");
      this.tvExpired.setText(Html.fromHtml(StringUtils.getExpiredString(a.a().f()))); //这里就是调用f()方法的地方
      if (a.a().f() == 1) {
            this.tvBtnBuyVip.setVisibility(8);
      } else {
            this.tvBtnBuyVip.setVisibility(0);
      }
    .... //中间的代码省略
if (a.a().g() == 2) {
            this.tvMemberName.setText("体验会员");
            this.tvMemberName.setTextColor(getResources().getColor(R.color.userTiYan));
            this.ivMemberIcon.setImageResource(R.drawable.icon_v_2);
            return;
      }




点进StringUtils.getExpiredString()方法中看下,他做了些什么

public static String getExpiredString(long j) {
      if (!a.a().d()) { //a.a().d() 这个就是判断是否为会员
            return "<font color='#fe022b'>已到期</font>";
      }
      if (j == 1) { //j就是我们的过期时间。如果是1 就是无限制
            return "<font color='#ffb335'>无限制</font>";
      }
      if (j == 0) {
            return "<font color='#fe022b'>已到期</font>";
      }
      StringBuilder stringBuilder = new StringBuilder();
      stringBuilder.append("<font color='#ffb335'>");
      stringBuilder.append(d.a(new Date(j), "yyyy-MM-dd"));
      stringBuilder.append("</font>");
      return stringBuilder.toString();
    }



那么到期时间的值解决了,那是否为会员的呢?我们点进a.a().d()中去看看(注意:我在jadx中点不进去,所以又去了JEB...)

public boolean d() {
      boolean v0 = this.k > 0 ? true : false; //this.k 的值就是json中iv的值。这里也就是说,只要iv>0就是会员。所以我们iv的值就设置>0的数
      return v0;
    }


我们看看“体验会员”是怎么判断的。
a.a().g()方法的代码如下:

public int g() {
      return this.k;
}


到这里我们会员的取值也出来了,必须>0 。并且不能为2 所以我们就设置成1吧~~ 其他的值可自试~~

三、编写插件
   Xposed的编写就不示范了,百度一大把。我的代码如下(代码写的很垃圾,勿喷。只为实现功能):
   
       public class Hook implements IXposedHookLoadPackage{    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
      if("com.bly.dkplat".equals(lpparam.packageName)){
            XposedBridge.log("多开分身 开始Hook...");

            XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                  ClassLoader classLoader = ((Context) param.args).getClassLoader();
                  Class<?> aClass = classLoader.loadClass("com.bly.dkplat.a.a");
                  if(aClass!=null){
                        //过期时间
                        XposedHelpers.findAndHookMethod(aClass, "a", long.class, new XC_MethodHook() {
                            @Override
                            protected void beforeHookedMethod(MethodHookParam param) throws Throwable { //在方法执行前执行
                              
                              param.args = 1L; //设置第一个参数值
                              
                            }
                        });
                        //是否为vip
                        XposedHelpers.findAndHookMethod(aClass, "a", int.class, new XC_MethodHook() {
                            @Override
                            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {//在方法执行前执行
                              
                              param.args = 1; //设置第一个参数值
                              
                            }
                        });
                  }else{
                        XposedBridge.log("多开分身 class not found please restart app ...");

                  }
                  XposedBridge.log("多开分身 Hook结束...");

                }
            });
      }
    }
}
   

四、结果


最后,希望大家多多评分~~ 最后还是得问问,论坛啥时候上MarkDown插件~~不然帖子好丑。。。再多说一句,本文仅做知识交流,如果大家喜欢这款App,希望大家能够支持正版。
感谢各位表哥观看~~~





天玄 发表于 2018-8-18 01:15:14

谢谢大表哥分享心得体会

wgz001 发表于 2018-8-18 08:20:13

表哥   大安卓玩的6啊   有时间带我飞

Bu弃 发表于 2018-8-18 08:23:24

wgz001 发表于 2018-8-18 08:20
表哥   大安卓玩的6啊   有时间带我飞

表哥缪赞了。带飞还是得看ROOKING表哥阿,毕竟是全能型选手@rooking

lihong2322 发表于 2018-8-18 09:08:04

好东西果断收藏!!

Rooking 发表于 2018-8-18 09:53:05

表哥深藏不露啊 羡慕羡慕 学习学习

Tue7825 发表于 2018-8-18 13:31:25

谢谢分享,看着好深奥{:lol:}

bpzm1987 发表于 2018-8-18 20:10:49

表哥好厉害的样子!

逸启i 发表于 2018-8-21 22:30:32


好东西果断收藏



alwnfin 发表于 2018-8-22 00:40:14

分享精神,是最值得尊敬的!
页: [1] 2
查看完整版本: “某开分身”永久VIP Xposed实现及分析