欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]Xposed去除微信朋友圈广告

[原创]Xposed去除微信朋友圈广告

一、背景:最近发现微信朋友圈的广告有点多,有时前十条就出现2条广告,还是针对性投放的,于是想着怎么去除广告。

本次研究用的是微信谷歌商店版,版本名是7.0.0,版本号是1363,

云盘地址,反编译工具是mt管理器" class="anchor" href="#云盘地址,反编译工具是mt管理器">云盘地址,反编译工具是MT管理器

二、首先,广告和说说,明显的区别就是条目右上角有个广告标识,还可以进行点击,这就好办了。

[图片:87395009bc3e492b877e3af29cedacd2.png]

1、用android sdk里的 monitor 工具进行 dump view hierarchy 分析,发现朋友圈的列表用的是ListView,条目中广告标识的 layout 是 LinearLayout ,resource id 是 eb9

2、接着就是MT管理器发挥作用的时候了,对微信的安装包里的 resources.arsc 进行 id 过滤,找到 eb9 对应的16进制id 是 7F111B0F。

接下来用MT管理器的 Dex编辑器++ 选择所有的dex文件,搜索 7F111B0F 的16进制整数,结果里 7F111B0F 对应的名称都是 ad_info_ll。

于是再对 ad_info_ll 进行代码搜索,发现搜索结果多了一条,在 com.tencent.mm.plugin.sns.ui.bb 里被使用,打开该类并反编译成java代码

public bb(View view) {

   this.view = view;

   ab.i("MicroMsg.TimeLineAdView", "adView init lan " + this.mRP);

   this.qGB = (TextView) this.view.findViewById(f.ad_info_tv);

   this.qGC = (TextView) this.view.findViewById(f.ad_link_tv);

   this.qGD = this.view.findViewById(f.ad_info_tv_arrow);

   this.qGE = this.view.findViewById(f.ad_lbs_icon_tv);

   this.qGA = (LinearLayout) this.view.findViewById(f.ad_info_ll);

   this.qGB.setText(" " + this.view.getResources().getString(j.sns_ad_tip) + " ");

}

构造函数里对相关 view 进行查找,并且该类还有一个 setVisibility 方法,广告标识要显示,传入的就应该是View.VISIBLE,对应的 int 的值为 0

3、编写Xposed模块对 setVisibility 方法进行hook,打印 Log.getStackTraceString(new Throwable()) 的结果

发现 com.tencent.mm.plugin.sns.ui.item.BaseTimeLineItem 的 方法 a(BaseViewHolder baseViewHolder, int i, n nVar, TimeLineObject timeLineObject, int i2, au auVar) 有相关代码

av avVar = (av) auVar.cky().cim().get(Integer.valueOf(i));

...

...

if (avVar.qkM) {

   baseViewHolder.pJU.setVisibility(8);

} else {

   ab.i("MicroMsg.BaseTimeLineItem", "adatag " + avVar.qDU);

   baseViewHolder.pJU.x(Long.valueOf(avVar.qDK), new com.tencent.mm.plugin.sns.data.b(baseViewHolder.pJU, baseViewHolder.position, avVar.qml, avVar.qDK, avVar.qDQ));

   baseViewHolder.pJU.a(avVar.qDT, avVar.qDS);

   baseViewHolder.pJU.setVisibility(0);

   if (baseViewHolder.qzu != null && baseViewHolder.qzu.getVisibility() == 0) {

      if (baseViewHolder.pJU.qGC.getVisibility() != 0) {

         obj = 1;

      } else {

         obj = null;

      }

      if (obj != null) {

         LayoutParams layoutParams = (LayoutParams) baseViewHolder.qzu.getLayoutParams();

         layoutParams.setMargins(layoutParams.leftMargin, BackwardSupportUtil.b.b(this.mActivity, 0.0f), layoutParams.rightMargin, layoutParams.bottomMargin);

         baseViewHolder.qzu.setLayoutParams(layoutParams);

      }

   }

}

上面的第一行代码对方法的最后一个参数进行了一系列调用,返回 com.tencent.mm.plugin.sns.ui.av 类的对象。

接着对 av 对象的成员变量 qkM 进行判断,这里 baseViewHolder.pJU 就是 com.tencent.mm.plugin.sns.ui.bb 的对象,对判断的结果分别传 0 和 8 进行显示和隐藏。

于是我们判断一个 av 实例对应一个朋友圈条目,而 qkM 变量可以判断当前条目是否是广告

4、对代码 av avVar = (av) auVar.cky().cim().get(Integer.valueOf(i)) 进行分析,找到 auVar 应对的类 com.tencent.mm.plugin.sns.ui.au 并进行分析, 发现 cky 方法返回的是成员变量 qBs,

public final w cky() {

   return this.qBs;

}

而该变量又是在该类的构造方法进行赋值的,对构造方法进行hook,打印调用栈。发现在类 com.tencent.mm.plugin.sns.ui.a.a 的构造方法

public a(MMActivity mMActivity, ListView listView, com.tencent.mm.plugin.sns.ui.d.b bVar, i iVar, String str, b bVar2) {

   this.qHT = new au(mMActivity, listView, bVar, iVar, this);

   this.qHT.qta = true;

   if (bVar2 == null) {

      bVar2 = new c();

   }

   this.qHU = bVar2;

   this.qHU.a(mMActivity, this.qHT, str);

   b bVar3 = this.qHU;

   com.tencent.mm.vending.f.a.i("Vending.ForwardVending", "Vending.setRangeSize(%s)", new Object[]{Integer.valueOf(10)});

   bVar3.a = 10;

   this.qHU.addVendingDataChangedCallback(this.qHW);

}

中进行实例化,传入的参数值是 this,即该类变量本身,找到代码 av avVar = (av) auVar.cky().cim().get(Integer.valueOf(i)) 中 cim 方法返回的是成员变量 qHU

public final Vending cim() {

   return this.qHU;

}

该变量在上面的构造函数中被赋值,不是参数中类 com.tencent.mm.plugin.sns.ui.a.b.b 的实例 bVar2,就是实例化一个 com.tencent.mm.plugin.sns.ui.a.b.c。

而类 com.tencent.mm.plugin.sns.ui.a.a 继承于 BaseAdapter 。

上面说过,朋友圈的列表展示用的是ListView,而 BaseAdapter 就是 ListView 的适配器,用于展现条目,而BaseAdapter 的 getView 返回的就是一个条目的视图。

分析该方法在 com.tencent.mm.plugin.sns.ui.a.a 中的实现,参数 i 表示列表中的位置,从0开始。其中成员变量 qHT 是 上面分析过的类com.tencent.mm.plugin.sns.ui.au 的一个实例,调用 h 方法返回条目的视图

public final View getView(int i, View view, ViewGroup viewGroup) {

   return this.qHT.h(i, view);

}

这下有点思路了,根据类 au 的成员变量 qkM 可以分析是否是广告,而 au 可从代码 av avVar = (av) auVar.cky().cim().get(Integer.valueOf(i)) 中获得,在调用链最后的 get 方法中保存广告条目,在 getView 中判断是否是广告,是广告就返回空视图,不是就继续调用 this.qHT.h(i, view),不就可以去除广告了吗。

回到正题,我们分析到了调用链的 cim 方法,方法返回的是成员变量 qHU,找到 get 方法需要找到对应的类。而qHU在上面的构造函数中赋值,不是最后一个参数就是实例化一个c。

分析发现,类com.tencent.mm.plugin.sns.ui.a.b.c 继承于 com.tencent.mm.plugin.sns.ui.a.a,而com.tencent.mm.plugin.sns.ui.a.a 实现接口 com.tencent.mm.plugin.sns.ui.a.b.b,在这些类中都未发现 get 方法,继续寻找继承关系,在 com.tencent.mm.vending.base.b 中发现了 get 方法

public final <T> T get(int i) {

   if (this.c != 0) {

      return super.get(Integer.valueOf(i));

   }

   a.e("Vending.ForwardVending", "mCount is 0, why call get()?", new Object[0]);

   return null;

}

调用父类 com.tencent.mm.vending.base.Vending 的同名方法

public <T> T get(_Index _Index) {

   return a((Object) _Index);

}

private _Struct a(_Index _Index) {

   Looper myLooper = Looper.myLooper();

   if (myLooper != this.c && myLooper != this.d) {

      throw new IllegalAccessError("Call from wrong looper");

   } else if (this.g.get()) {

      return null;

   } else {

      i lock = getLock(_Index);

      if (invalidIndex(_Index)) {

         return lock.b;

      }

      if (myLooper == this.c) {

         return b(lock, _Index).b;

      }

      a(lock, (Object) _Index);

      return lock.b;

   }

}

private boolean a(i<_Struct, _Index> iVar, _Index _Index) {

   synchronized (iVar.c) {

      if (!iVar.f || iVar.d || iVar.e) {

         this.q = true;

         Object resolveAsynchronous = resolveAsynchronous(_Index);

         this.q = false;

         if (iVar.g) {

            return false;

         }

         a((i) iVar, (Object) _Index, resolveAsynchronous);

         return false;

         }

      return true;

   }

}

protected abstract _Struct resolveAsynchronous(_Index _Index);

上面 get 方法调用一个参数的 a 方法,一个参数的 a 方法调用两个参数的 a 方法,最终调用需要子类实现的 resolveAsynchronous 方法。

在子类 com.tencent.mm.vending.base.b 中发现了 resolveAsynchronous 方法,调用的是子类的 Cx 方法

protected Object resolveAsynchronous(Object obj) {

   return Cx(((Integer) obj).intValue());

}

protected abstract _Struct Cx(int i);

在子类 com.tencent.mm.plugin.sns.ui.a.b.a 中发现了 Cx 方法,

public final Object Cx(int i) {

   return Cw(i);

}

该方法调用了同类的 Cw 方法,改方法的代码有点长,就不贴了,最终返回了我们需要的类 av 的实例

5、终于可以写代码了,主要逻辑是获取 com.tencent.mm.plugin.sns.ui.a.b.a 的 Cw 方法返回值 av 的实例,判断该实例的成员变量 qkM ,如果为 true 就表示是广告,保存下来。在com.tencent.mm.plugin.sns.ui.a.a 中判断当前条目是否在上面保存的结果中,如果存在就返回空视图,返回继续调用该方法体。这里就不处理缩进了

    private Context context;

    private final Set<Integer> adSet = new HashSet<>();

    private void hookA(final XC_LoadPackage.LoadPackageParam lpParam) {

        try {

            String className = "com.tencent.mm.plugin.sns.ui.a.a";

            final Class clazz = lpParam.classLoader.loadClass(className);

            if (clazz != null) {

                Constructor[] constructors = clazz.getDeclaredConstructors();

                for (Constructor constructor : constructors) {

                    if (constructor.getParameterTypes().length == 6) {

                        XposedHelpers.findAndHookConstructor(clazz, constructor.getParameterTypes()[0],

                                constructor.getParameterTypes()[1], constructor.getParameterTypes()[2],

                                constructor.getParameterTypes()[3], constructor.getParameterTypes()[4],

                                constructor.getParameterTypes()[5], new XC_MethodHook() {

                                    @Override

                                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {

                                        context = (Context) param.args[0];

                                        adSet.clear();

                                    }

                                });

                        break;

                    }

                }

                XposedHelpers.findAndHookMethod(clazz, "getView", int.class, View.class, ViewGroup.class,

                        new XC_MethodReplacement() {

                            @Override

                            protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {

                                if (adSet.contains(param.args[0])) {

                                    return new View(context);

                                }

                                return XposedBridge.invokeOriginalMethod(param.method, param.thisObject,

                                        param.args);

                            }

                        });

            } else {

                LogUtil.e(TAG, "class " + className + " not found");

            }

            className = "com.tencent.mm.plugin.sns.ui.a.a$1";

            final Class dataChangeClazz = lpParam.classLoader.loadClass(className);

            if (dataChangeClazz != null) {

                XposedHelpers.findAndHookMethod(dataChangeClazz, "cll", new XC_MethodHook() {

                    @Override

                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

                        super.beforeHookedMethod(param);

                        adSet.clear();

                    }

                });

            } else {

                LogUtil.e(TAG, "class " + className + " not found");

            }

        } catch (Throwable e) {

            LogUtil.e(TAG, "hookA err:" + Log.getStackTraceString(e));

        }

    }

    private void hookC(final XC_LoadPackage.LoadPackageParam lpParam) {

        try {

            final String className = "com.tencent.mm.plugin.sns.ui.a.b.a";

            final String avClassName = "com.tencent.mm.plugin.sns.ui.av";

            final Class clazz = lpParam.classLoader.loadClass(className);

            final Class avClazz = lpParam.classLoader.loadClass(avClassName);

            if (avClazz == null) {

                LogUtil.e(TAG, "class " + className + " not found");

                return;

            }

            final Field isAdField = XposedHelpers.findFieldIfExists(avClazz, "qkM");

            if (clazz != null) {

                XposedHelpers.findAndHookMethod(clazz, "Cw", int.class, new XC_MethodHook() {

                    @Override

                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {

                        super.afterHookedMethod(param);

                        Object result = param.getResult();

                        if (result == null) {

                            return;

                        }

                        boolean isAd = (boolean) isAdField.get(result);

                        if (isAd) {

                            int position = (int) param.args[0];

                            adSet.add(position);

                            LogUtil.e(TAG, "position: " + position + " is ad");

                        }

                    }

                });

            } else {

                LogUtil.e(TAG, "class " + className + " not found");

            }

        } catch (Throwable e) {

            LogUtil.e(TAG, "hookA err:" + Log.getStackTraceString(e));

        }

    }

wechatsnsad" class="anchor" href="#所有源代码已上传 wechatsnsad">所有源代码已上传 WeChatSnsAd

最后,腾讯官方的减少个性化广告地址:https://privacy.qq.com/ads/optout.html

在当前页登录及后续操作视频教程:https://tccos.myworld.fun/upload/2019-09-14_15-41-39-169.mp4

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功