某博国际版3.1.6/3.1.8去广告

前些天iPhoneSE正式退役,换了一台安卓机做主力机,三星 A60 元气版,刷微博的时候总是刷到非常低俗恶心的广告,网上经常看到去广告版的微博,全都是被加固过,怕被人暗算,不敢用,周末就自己搞了一下。之前微博平白无故把老子的号给封了,这是第二篇杠微博的文章。

额外多说几句,本文首发于2019.6.11,网上的去广告版有些被加固过,不知道作者有没有夹杂私货,我是不敢用,不建议使用。

一、简介

经提议,先放链接吧:直接下载: https://github.com/LeadroyaL/weico-no-ads 贴几张图,让你们看看广告是多么恶心,主要是我懒得截图,网友截的图:

如果是好看点的广告可以忍,但这个广告真滴是低俗,本文从技术上讲如何一步一步去掉广告。

版本:3.1.6 MD5 (weico.apk) = a3779d34ea636e7f3469b0e56b788c94 github 地址,可以看每次commit的区别:https://github.com/LeadroyaL/weico-no-ads 以及有两个 tag,3.1.6和3.1.8,版本之间基本没有区别,只是高版本移除掉了BaseFragmentActivity 里启动广告的代码。

直接下载: https://github.com/LeadroyaL/weico-no-ads/raw/master/weico-patch.apk

2019.6.23:因为 3.1.6原版中存在【移动网络卡死】的 bug,官方在3.1.8中修复。上文的链接已更新,仍然可以使用,下载回来的是3.1.8版本、并非3.1.6版本。

二、安卓重打包技术的简介

讲一些基础知识,安卓的 apk 是通过包名来确认唯一性的,并且开发者会长期使用同一份签名来发布它,这个签名不可伪造,用来证明这个包确实是作者颁发的。如果我们对其进行修改的话,是肯定会破坏这个签名的,所以只能卸载掉微博然后再安装我们自己的。

第一步就用 AndroidStudio 的指引,创建一个我们自己的证书,这个步骤就跳过了,太简单了,假设我们生成的文件叫official.jks ,放好它。

第二步是删掉原先的签名,apk 的签名位置是META-INF 目录,使用

zip weico.apk -d "META-INF/*"

来删除 第三步是将 apk 对齐,要用到 AndroidSDK 里build-tools 里的工具,

$ANDROID_SDK/build-tools/28.0.3/zipalign 4 weico.apk weico-align.apk

第四步是将它签名,同样要用到AndroidSDK 里 build-tools 里的工具,

$ANDROID_SDK/build-tools/28.0.3/apksigner sign --ks ~/Android_code/official.jks weico-align.apk

这时候得到的 apk 就可以安装了,签名会变成我们的签名。

三、绕过重打包检测

参考这篇文章,挺有意思的,少走一些弯路。https://www.jianshu.com/p/79c3adfa483b 一般来说,app 都会做做一些基础防护,例如检测当前 app 是否被修改过,当然这么做不是为了防止爱折腾的人乱搞,而是防止小白用户安装了被人动过手脚的app。微博国际版也不例外,我们篡改签名的 app 装上就 crash了。

crash 的栈回溯和 app 相关的是这两句

1
2
06-05  #20 pc 0000200f /data/app/com.weico.international-1/lib/arm/libutility.so (Java_com_sina_weibo_security_WeiboSecurityUtils_getIValue+42)
06-05 #21 pc 004fa5db /data/dalvik-cache/arm/data@app@com.weico.international-1@base.apk@classes.dex
1
2
3
4
if ( ((int (*)(void))(*a1)->PushLocalFrame)() < 0 )
return 0;
if ( !sub_1C60() )
((void (__fastcall *)(JNIEnv *, int, _DWORD))(*v6)->ThrowNew)(v6, v5, 0);

而这个1C60就是简单的校验,然后返回true/false

于是我将对这个函数的调用改掉,直接对 R0 赋值为 1。

1
2
3
4
5
6
7
BL sub_1C60
===>
MOV R0, #1; 0120
NOP ; 00bf

PatchWord(0x1ffe,0x2001)
PatchWord(0x2000,0xbf00)

同样,另一处引用

1
2
PatchWord(0x1E86,0x2001)
PatchWord(0x1E88,0xbf00)

然后继续测,这时候不 crash 了,登录时候给弹了个框,而且和输入的用户名密码没有关系,客户端身份校验失败,如图

那篇文章里说是服务器动态下发的,我不服,因为我 grep 到了字符串,但经过一番 dump 后,确实是服务器下发的,通过对 toast 抛出异常的方式,我找到了弹 toast 的地方。

1
2
3
4
W/System.err:     at com.weico.international.manager.UIManager.showSystemToast(UIManager.java:308)
W/System.err: at com.weico.international.activity.SinaLoginMainActivity$13.onSuccess(SinaLoginMainActivity.java:782)
W/System.err: at com.weibo.sdk.android.api.WeicoCallbackString.success(WeicoCallbackString.java:57)
W/System.err: at com.weibo.sdk.android.api.WeicoCallbackString.success(WeicoCallbackString.java:31)


onSuccess 解析服务器返回的字符串,显然不能走 return 的分支,而在checkLoginResponseForWeibo 里可能抛出异常,然后被捕获并且 toast 出来。

为了验证我们的猜想,我拿到了 onSuccess 的第一个参数,抓个包,果然是这样

1
06-05 checkLoginResponseForWeibo  {"errmsg":"客户端身份校验失败","errno":-105,"errtype":"DEFAULT_ERROR","isblock":false}

下一步就得追为什么服务器会返回这个神奇的字符串,肯定是客户端告诉它了,前面有三个不认识的函数,稍微瞅一眼

1
2
3
WeicoSecurityUtils.securityPsd(Context,psw)
WeicoSecurityUtils.WeiboPin(Context)
WeicoSecurityUtils.calculateS

使用hook 对比一下原版和改版的区别

|api | origin | patch |
|securityPsd|xxx|"签名校验失败"|
|WeiboPin|xxx|"签名校验失败"|

这次我们使用修改 plt 表的方式,一劳永逸

2019年10月4日:但如果其他library也调用这个函数,就会导致其他library调用到的并没有被patch,还是老老实实patch函数体比较好。

1
2
3
4
MOVS R0, #1
BX LR
PatchWord(0x4e2c, 0x2001)
PatchWord(0x4e2e, 0x4770)

然后把文件放回去,一切就完美啦!

1
2
3
4
zip weico.apk -d "lib/armeabi/libutility.so"
zip weico.apk -z lib/armeabi/libutility.so
zip weico.apk -d "lib/armeabi/libnative-lib.so"
zip weico.apk -z lib/armeabi/libnative-lib.so

四、去掉开屏的广告

屏幕上有四个大字,点击跳过x

1
2
3
4
5
6
7
8
9
10
➜  weico grep "点击跳过" * -R
res/values-zh-rCN/strings.xml: <string name="click_to_skip">点击跳过%d</string>

➜ weico grep "click_to_skip" * -R
res/values/public.xml: <public type="string" name="click_to_skip" id="0x7f0f00e5" />smali_classes2/com/weico/international/R$string.smali:.field public static final click_to_skip:I = 0x7f0f00e5

➜ weico grep "0x7f0f00e5" * -R
res/values/public.xml: <public type="string" name="click_to_skip" id="0x7f0f00e5" />
smali_classes2/com/weico/international/activity/NewSplashActivity.smali: const v1, 0x7f0f00e5
smali_classes2/com/weico/international/R$string.smali:.field public static final click_to_skip:I = 0x7f0f00e5

找到目标:NewSplashActivity 它有很多处引用,有时候Intent 里会传onlyFinishSelf。

如果是 true,就显示后自杀;如果是 false,就登录或进入主界面。

看下引用,有这几处:

LogoActivity.openGDTAD --> 第一次启动时的广告,把doWhatNext里的返回"GDTAD"改成返回"main"

ProcessMonitor.onBackgroundToForeground --> 显示后自杀,直接删掉

BaseFragmentActivity.onResume --> 显示后自杀,直接删掉

SplashActivity.onFailedToReceiveAd -->好像没用,暂时不管了

具体操作看commit。

五、去掉时间线里的广告

这个其实是最难的部分,尝试了如下 N 种不同的方式,最后使用了最复杂的方式,在数据上进行过滤。

抓一下大致位置,发现和这两个 Activity 有关,一个是详情页,一个是时间线的 Adapter。

1
2
3
4
5
6
7
res/layout/ad_timeline_layout.xml
res/layout/layout_timeline_head.xml:<include android:id="@id/ad_timeline_header" layout="@layout/ad_timeline_layout" />
res/layout/item_timeline_adcard.xml: <include layout="@layout/layout_timeline_head" />
res/values/public.xml: <public type="layout" name="item_timeline_adcard" id="0x7f0b012e" />

smali_classes2/com/weico/international/activity/SeaStatusDetailActivity.smali
smali_classes2/com/weico/international/adapter/TimeLineRecyclerAdapter.smali

尝试的错误方案如下:

思路1:发现code==23 是广告(其实并不是),在附近修改,不进行渲染。

每次修改后,要么界面全白,要么时间线里的【这是广告】的提醒消失了,感觉有点问题,尝试了十来处都没有成功。

当时是周六下午,我以为我成功了,就和妹子睡午觉,中途醒了刷一下微博,看到一条广告,气得我马上起床打开电脑继续去搞,睡意全无。 两个失败的截图,一般都是这两种失败。要么白屏,要么广告的修饰 UI 被我去掉了,很气。

思路 2:调用主动让广告消失的 API

因为广告可以被关掉,所以跟一下让广告消失的事件,用EventBus 吧,主动调用EventBus.getDefault().post(new HomeTimelineNeedDeleteStatusEvent(this.$statusId)); 似乎因为没有通知到UI 更新,并没有效果。

思路3:在数据源进行过滤

其实这种方案我最开始就想到了,只是觉得有点复杂,懒得写一堆 smali 代码,没办法,最后的出路了。

第一处:TimeLineRecyclerAdapter 的构造方法,输入是一个 list,我写完代码发现这个 list 其实长度为 0;

第二处:com.jude.easyrecyclerview.adapter.RecyclerArrayAdapter 的 add 添加单个元素,然后发现没人调它,基本每次都是添加一大堆的;

第三处:com.jude.easyrecyclerview.adapter.RecyclerArrayAdapter 的 addAll 批量添加,这个是生效的,我们可以判断元素类型然后打印出它的各个属性。最开始我按照 code 过滤,发现广告并不是23,之后就配合打印出文本内容,发现了一个叫isUVEAd 的布尔值,当它是广告时候是 true。

修改后的逻辑如图

1
2
3
4
5
6
06-07 16:48:09.725 25537 25537 E ccttff  : 4
06-07 16:48:09.725 25537 25537 E ccttff  : @神楽坂南音@_青泽今年也是绝赞摸鱼中
06-07 16:48:09.725 25537 25537 E ccttff  : 4
06-07 16:48:09.725 25537 25537 E ccttff  : 【悲剧!DC《沼泽怪物》才播一集就将被砍 烂番茄92%】DC最新美剧《沼泽怪物》刚刚于5月31日首映,口碑不错,烂番茄新鲜度达92%,只是没想到该剧才刚刚播出第一集,就确认了被砍的命运。06-07 16:48:09.725 25537 25537 E ccttff  : 据外媒Variety报道,流媒体平台DC宇宙取消了《沼泽怪物》的第二季预订,第一季的后续内容将继续播出。有消息人士称
<red>06-07 16:48:09.725 25537 25537 E ccttff  : 4</red>
<red>06-07 16:48:09.725 25537 25537 E ccttff  : 天天在线还能赚零花钱,空闲时间看一看新闻[偷笑]玩一玩,链接放这了:httpxxxxxxwjT不知道的赶快了解下[酷]聚头条</red>

这时发现广告和正常微博的 type 是一样的,文本类的都是 4,并不是预期的 23,不是通过这个字段区分是否为广告的。

其实是 isUVEAd 来区分广告的

1
2
06-07 17:03:43.267  4351  4351 E ccttff : 2
06-07 17:03:43.267  4351  4351 E ccttff : true

成功!确确实实从数据源头上把恶心广告给去掉了!

这里有个细节,就是一个 java 小知识,使用 iterator 时千万不要对 Collection 进行移除操作,需要使用 iterator 本身的 remove 操作。

1
2
3
4
5
6
7
8
9
10
2019-06-07 17:12:34.794 7563-7563/? E/ccttff: true
2019-06-07 17:12:34.795 7563-7563/? E/ccttff: remove it!
2019-06-07 17:12:34.795 7563-7563/? E/Event: Could not dispatch event: class com.weico.international.flux.Events$HomeTimelineLoadEvent to subscribing class class com.weico.international.fragment.IndexFragment
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.next(ArrayList.java:860)
at com.jude.easyrecyclerview.adapter.RecyclerArrayAdapter.addAll(Unknown Source:18)
at com.jude.easyrecyclerview.adapter.RecyclerArrayAdapter.setItem(RecyclerArrayAdapter.java:404)
at com.weico.international.fragment.IndexFragment.onEventMainThread(IndexFragment.java:630)
at java.lang.reflect.Method.invoke(Native Method)
at de.greenrobot.event.EventBus.invokeSubscriber(EventBus.java:498)

错误的修改会触发了ConcurrentModificationException。

六、去掉侧边栏的广告

侧边栏的推广

1
2
3
4
21     <group android:id="@id/menu_setting_group3">
22 <item android:icon="@drawable/ic_siderbar_gifts" android:id="@id/nav_daily_benefit" android:title="@string/daily_benefit" />
23 <item android:icon="@drawable/ic_sidebar_recommend" android:id="@id/nav_recommend" android:title="@string/slide_recommend_miaowu" />
24 </group>

smali_classes2/com/weico/international/activity/MainFragmentActivity.smali

七、拒绝更新

smali_classes2/com/weico/international/activity/MainFragmentActivity.smali

checkUpdateForSinaForceUploadVersion-> 删掉

smali_classes2/com/weico/international/other/UpdateDownloadManager.smali

download -> 删掉

八、总结

顿时没有广告,神清气爽,呵呵,曾经微博国际版的宣传点就是无广告清爽,结果自己活成了自己讨厌的那副样子,一点都不为用户考虑,辣鸡!!!