飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 8238|回复: 18

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

  [复制链接]
  • TA的每日心情
    开心
    2024-4-8 22:23
  • 签到天数: 110 天

    [LV.6]常住居民II

    发表于 2018-8-15 17:20:36 | 显示全部楼层 |阅读模式
    本帖最后由 Bu弃 于 2018-8-16 13:36 编辑

    Hello Everyone~ 在论坛看到Rooking表哥发的多开分身最新版7.2永久VIP版[size=10.5000pt]”https://www.chinapyg.com/thread-120623-1-1.html)一时手痒,自己也抽空分析了下,使用Xposed实现永久VIP。下面是分析全过程~~ 另:特别感谢周年群中的大表哥们提供脱壳后的dex文件。
    一、观察
    在着手分析APK前,我们先装上app,观察它的一些特征。
    图片1.png
    “个人中心”页面,有显示是否为会员,以及到期时间。如果我们第一次运行的时候是断网的,会提示请连接网络什么的。在这里,我们可以确定一件事:app在打开时会请求服务器,获取到期时间以及是否为会员,还有广告等等。
    二、分析
    [size=14.0000pt]   上面说到了,应用会联网获取到期时间等信息,那么我们可以通过抓包的方式,来确定app发送的请求。然后通过该url进行全局搜索。更加容易定位到关键处。这里我使用Fiddler来抓包。
    Fiddler配置我就不说了,百度一大堆。抓包结果如下:
    图片2.png
    为什么确定是这些?因为该app的官网就是91xxx.cn.接下来一个个看,于是发现该连接最可疑..
    图片3.png
    Jadx打开脱壳后的dex(由于某些原因,这里就不传dex)Navigation->Text Search
    图片4.png
    搜索链接/ServerV60?fn=it”结果有很多。如下
    图片5.png
    我们点开第一个观察先。代码如下:
    [Java] 纯文本查看 复制代码
    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,找到这个类。代码如下:
    [Java] 纯文本查看 复制代码
    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读取的值传过去。我们点进去看看。代码如下:
    [Java] 纯文本查看 复制代码
    public void a(String arg1) {
            this.l = arg1;  //赋值给l。
     }
    
    

    从这大概可以看出来了大致流程了。即,从服务器读取数据,然后解析json,给类中的属性赋值。而后就是通过该类中的属性进行判断和显示。
    到这,我们已经有了Xposed的思路了。只要Hook了给类中属性赋值的方法就可以达到目的了。
    哦,对了,还有个东西忘了说了。既然我们知道只要Hook了属性赋值的地方就可以了,那么这个值是多少合适?既然我们已经知道过期时间是通过值“l”判断的,那么我们看看有什么地方调用了它。在JEB中选中该属性,右键->Cross references 查看调用
    图片6.png
    这里获取值,按道理应该是调用get方法,所以该方法应该没有参数。于是锁定了最后一个f()方法。同样,选中该方法,右键->Cross references 查看调用.然后这里又有个问题,我的JEB是没有反编译该方法的。所以只能在jadx中看了。在jadx中找到f()方法,选中,右键->Find Usage
    图片7.png
    我们发现第三个有点可疑,点过去看下。代码如下:
    [Java] 纯文本查看 复制代码
    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()方法中看下,他做了些什么
    [Java] 纯文本查看 复制代码
    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...)
    [Java] 纯文本查看 复制代码
    public boolean d() {
            boolean v0 = this.k > 0 ? true : false; //this.k 的值就是json中iv的值。这里也就是说,只要iv>0就是会员。所以我们iv的值就设置>0的数
            return v0;
        }
    
    

    我们看看“体验会员”是怎么判断的。
    a.a().g()方法的代码如下:
    [Java] 纯文本查看 复制代码
    public int g() {
            return this.k;
     }
    
    

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

    三、编写插件
    [size=14.0000pt]     Xposed的编写就不示范了,百度一大把。我的代码如下(代码写的很垃圾,勿喷。只为实现功能):
    [size=14.0000pt]     
    [Java] 纯文本查看 复制代码
           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[0]).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[0] = 1L; //设置第一个参数值
                                    
                                }
                            });
                            //是否为vip
                            XposedHelpers.findAndHookMethod(aClass, "a", int.class, new XC_MethodHook() {
                                @Override
                                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {//在方法执行前执行
                                   
                                    param.args[0] = 1; //设置第一个参数值
                                    
                                }
                            });
                        }else{
                            XposedBridge.log("多开分身 class not found please restart app ...");
    
                        }
                        XposedBridge.log("多开分身 Hook结束...");
    
                    }
                });
            }
        }
    }
         


    四、结果
    [size=14.0000pt]   图片8.png

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




    评分

    参与人数 7威望 +53 飘云币 +81 收起 理由
    GenW + 1 + 1 支持bu弃表哥作品
    bpzm1987 + 4 分享精神,是最值得尊敬的!
    Tue7825 + 4 PYG有你更精彩!
    yosen2001 + 16 表哥威武,表弟学习了
    Rooking + 20 + 40 PYG有你更精彩!
    wkxq + 4 + 4 没有看明白,但支持你
    wgz001 + 20 + 20 感谢发布原创作品,PYG有你更精彩!

    查看全部评分

    本帖被以下淘专辑推荐:

    PYG19周年生日快乐!
  • TA的每日心情
    郁闷
    2024-4-16 18:05
  • 签到天数: 563 天

    [LV.9]以坛为家II

    发表于 2018-8-18 01:15:14 | 显示全部楼层
    谢谢大表哥分享心得体会
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2019-2-26 11:14
  • 签到天数: 459 天

    [LV.9]以坛为家II

    发表于 2018-8-18 08:20:13 | 显示全部楼层
    表哥   大安卓玩的6啊   有时间带我飞
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2024-4-8 22:23
  • 签到天数: 110 天

    [LV.6]常住居民II

     楼主| 发表于 2018-8-18 08:23:24 | 显示全部楼层
    wgz001 发表于 2018-8-18 08:20
    表哥   大安卓玩的6啊   有时间带我飞

    表哥缪赞了。带飞还是得看ROOKING表哥阿,毕竟是全能型选手@rooking
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2019-10-20 08:55
  • 签到天数: 55 天

    [LV.5]常住居民I

    发表于 2018-8-18 09:08:04 | 显示全部楼层
    好东西果断收藏!!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2019-3-25 15:21
  • 签到天数: 487 天

    [LV.9]以坛为家II

    发表于 2018-8-18 09:53:05 | 显示全部楼层
    表哥深藏不露啊 羡慕羡慕 学习学习
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2022-4-24 09:12
  • 签到天数: 317 天

    [LV.8]以坛为家I

    发表于 2018-8-18 13:31:25 | 显示全部楼层
    谢谢分享,看着好深奥
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    昨天 13:44
  • 签到天数: 1272 天

    [LV.10]以坛为家III

    发表于 2018-8-18 20:10:49 | 显示全部楼层
    表哥好厉害的样子!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2023-7-26 20:18
  • 签到天数: 68 天

    [LV.6]常住居民II

    发表于 2018-8-21 22:30:32 | 显示全部楼层
    好东西果断收藏


    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    昨天 14:38
  • 签到天数: 81 天

    [LV.6]常住居民II

    发表于 2018-8-22 00:40:14 | 显示全部楼层
    分享精神,是最值得尊敬的!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

    快速回复 返回顶部 返回列表