引言

这篇文章与其说是写一个去广告的工具,不如说是写一个自动化工具更为准确。我不会讲代码的细节,“一千个人眼里有一千个哈姆雷特”,每个人写代码的风格都不一样,最重要只有思路(实际上这个思路也并不高明,唯一的重点就是清楚原理),你们可以用喜欢且擅长的语言及方式来进行实现,不过最终我会放出自己的源代码(我的代码相对于单一目标的实现可能会有些繁杂,只需要一两百行的代码我写了两千行还不到头哈哈,所以在文中只会贴上需要的部分,想要阅读完整代码的可以上我的github,当然在这之前请记住”文明社会”这四个字)。

那么开始步入正题,我们要开发的是一款自动化去广告的工具,何为自动化,自动化就是解放双手,让程序完成需要你动手的一系列操作。那么,想要自动化就必须先知道正常手工是如何操作的,接下来,我们来探讨一下APK如何去广告这件事情。

本文所叙都是在APK没有加壳/加密或者已经完美脱壳/解密的情况下

本文作者:hluwa  首发i春秋,文中所述及成果仅作技术研究讨论,未经授权不允转载

如何添加广告

兵家云:“知己知彼,百战不殆”,假如你知道这个程序是如何被添加上广告的,那么你的后续操作将会轻松很多,因为你不必再花费大量的时间对广告SDK进行分析。我们先了解一下广告是怎样以一种形式存在,以Google的广告为例,Google的广告使用范围很广,在Google Play上无论是应用还是游戏,有很大部分都是使用其提供的广告组件。
在Google提供的Android集成开发环境Android Studio上,对着Project点击右键Open Module Setting然后可以看到这么一个东西:

这是什么呢?这是Google提供的广告SDK,勾选后他将会自动下载开发工具包并将其集成到你的Project上,没错,广告就是从这么一个SDK里来的,它就是我们的敌人!我们到他的官方网站可以看到接入指南(https://developers.google.com/admob/android/quick-start),可以看到加载广告的第一步就是初始化SDK

package ...import ...import com.google.android.gms.ads.MobileAds;

public class MainActivity extends AppCompatActivity {    ...    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713        MobileAds.initialize(this, "YOUR_ADMOB_APP_ID");    }    ...}

初始化的参数有一个ADMOB_APP_ID,这是开发者的凭证,填上这个ID才可以拿到属于你的那份广告收益。在页面的下半部分还可以看到其广告的几种类型,其实我猜市面上的大部分广告组件都是类似的:

  1. Banner:横幅广告,这种无论是在桌面端还是移动端都非常常见,它占用你屏幕的一小部分来显示一个横幅的广告视图,但是大多数情况下并不能关闭它;

  2. Interstitial:悬浮窗广告,这个在Html和Android上较为常见,它占用屏幕的面积并不固定,有可能是占用一半屏幕甚至是整个屏幕,不过用户却可以手动将他关闭(不能关闭的那叫流氓)。

  3. Rewarded Video:其实就是视频广告,占用全屏,而且你还得等他全部播放完才能关闭他,当然也有些只需观看一定时间即可。

Native暂时不做考虑,这是谷歌一种比较高级的广告形式(好像也并没有广泛使用?)。
想要接入这些广告也十分简单,比如Banner,你只要在布局文件上添加一个AdView然后像这样加载它即可

package ...

import ...import com.google.android.gms.ads.AdRequest;import com.google.android.gms.ads.AdView;

public class MainActivity extends AppCompatActivity {    private AdView mAdView;

    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);

        MobileAds.initialize(getApplicationContext(),            "ca-app-pub-3940256099942544~3347511713");

        mAdView = (AdView) findViewById(R.id.adView);        AdRequest adRequest = new AdRequest.Builder().build();        mAdView.loadAd(adRequest);    }    ...}

而Interstitial甚至都不需要添加View,只需要loadAd然后在需要的时候调用show()方法将他显示出来即可。
好了,就说这些,不然我都要以为我是Google的顶级广告形式 – 人工广告了,接下来谈谈去广告的方法。

传统的绿化方式

此处仅从APK本身入手,不讨论如Hook,Hosts等手段。

从代码的层面上,我们知道了广告如何添加,那么想要将其移除相信对大家也不是什么难事,一般去广告的流程大致是这样的:

反编译APK --> 移除相关代码 --> 重打包测试

对于移除相关代码,有多种实现方式,比如Banner,你完全可以将其visibility属性设置为GONE就能把他隐藏掉(虽然我没测试过是否有效,哈哈)。不过我更加偏向于删除其加载的入口调用,可以来实战演示一下,下面以ADM(Advanced Download Manager)为例,相信很多人都知道这个软件吧,Android上的下载神器。没去广告之前他是这样子的:

可以看到底部的横幅图片,这就是Banner广告。在上一节中我们知道它调用了AdView的loadAd方法来加载广告,那么我们只要找到这个方法的调用点,然后将其删除就可以让广告无法顺利加载出来。那么怎么做呢?按照国际惯例,首先是反编译APK,我这里使用Android Killer这个工具来进行反编译,然后你会得到一些smali文件和资源文件。关于逆向的一些基本知识我这里不在阐述,对逆向有兴趣的同学可以自己搜索资料学习。我们在Android Killer中搜索”Lcom/google/android/gms/ads/AdView;->loadAd”,然后会出现这么一些结果:

这里我只选择对Main.smali中的代码进行处理,至于为什么,请参考上上句话,当然,就算你将它们全部处理了也不会有什么影响。我对搜索出来的这两行代码整行删除,然后保存编译。可以看到Banner广告已经不会再加载了:

是不是感觉很简单?其实本来就没有什么难度,甚至比添加广告还要简单,对于Interstitial或者Rewarded Video也是一样,可以发现,他们都调用了一个叫做loadAd的方法,所以我们可以进行模糊搜索,例如搜索”;->loadAd(“,然后会出现较多的结果,可以针对性的进行处理,不过我想就算是全部处理也不会有多大的影响。
现在你已经知道了绿化广告的原理,在进行了多次的重复工作之后,你会发现,就算这是最简单快捷的方法,但是效率依然很低,并且工作都是重复的,因为大部分广告都是出于同一个SDK。那么,可以开始考虑让万能的程序帮你解决问题了!

自动化绿化方法

大佬的操作

编写一个简单的自动化处理工具并不难,只要清楚了工作原理并且有一点点编程的能力,就可以写出一个帮助你快速处理任务的程序。按照国际惯例,无论是手动还是自动,第一步都是先反编译,这里我们可以直接调用apktool或者baksmali来处理,关于工具的使用及调用的方法有兴趣可以自己研究,这并不是我要讲的内容。得到反编译的代码之后,按照国际惯例第二步,就是找到smali代码中调用loadAd的地方将其删除,实现的过程大致如下:

1. 遍历所有Smali文件读入2. 遍历每一行代码是否形如 invoke-xxxxx {v*} Lcom/google/android/gms/ads/xxxx;->loadAd 之类的调用代码3. 将识别到的代码行删除4. 重新写出Smali文件

最后就是国际惯例最后一步,重打包,同样可以利用Apktool或者Smali.jar将其回编译为APK或者Dex,然后进行签名、测试即可。这样一来效率就可以提高很多了,你只要等待若干秒的时间就可以实现去广告的目的。当然这种方法是有弊端的,如果遇到无法反编译或者回编译的情况,那么估计就要花费一般功夫了,并且对于一个追求极致的人来说,这种方法还不够快!具体代码我就不写了,因为我之前写过Smali相关的处理库(在我的github上的某个Repository中可以看到,虽然比较简陋,但是足以应付一些简单的需求),所以我对这个也没有多大的兴趣,我想做的是一种更加极致的操作。

骚操作

众所周知,Android程序大部分的代码是包含在classes.dex里面的,所谓的Smali代码也就是从classes.dex中的每一个字节翻译出来的,那么,实际上我们只要改动classes.dex文件中的1个或者N个字节,就可以完成如上相等的效果。Dex文件的每一个字节都代表着相关的含义,具体参照Google的官方文档Dex文件格式(https://source.android.com/devices/tech/dalvik/dex-format),虽然这些格式相关的数据并不是我们所关心的内容,但是我们必须依靠它来找到我们需要的关键位置–字节码(bytecode),bytecode是程序运行是真正执行的指令(Dalvik字节码 https://source.android.com/devices/tech/dalvik/dalvik-bytecode ),dex文件格式就是用来帮助系统定位到这些指令的位置。比如我们上文做提到的invoke-xxxxxx就有一套专属的字节码,如果我们找到它的位置,然后把字节码改成0×00,0×00是代表nop的字节码,nop就是什么都不干的意思,那么这不就是等同于将这条代码删除了吗?
既然如此,我们来整理一下这个程序的执行流程:

解析Dex文件 -> 遍历所有的字节码 -> 匹配所有符合自定义规则的位置 -> 将其全部改为0x00 -> 重建DexHeader -> 签名、测

我们可以先研究下如何遍历所有的字节码:

首先可以使用010 Editor来很方便的分析Dex格式

呃..焦点选中的那个地方就是一个方法的字节码..可见想要获取全部还是得花一点功夫的哈。那么,图中出现的结构体我们在程序中都必须解析出来。而至于Leb128类型的数据,可以参照我的代码,我的Leb128类实质是无符号的uleb128类型。

我们再研究一下invoke系列字节码的格式:

指令格式是这样子的:invoke-kind {vC, vD, vE, vF, vG}, methhome.php?mod=space&uid=136254 这就是在Smali中看到的格式
而字节码格式是这样子的:A|G|op BBBB F|E|D|C 而这个是从Hex文件中看到格式
不过由于dex程序是小端对齐,所以真实的表现形式是这样的:op|G|A BBBB D|C|F|E(应该没错吧?欢迎指正)

ACDEFG都是指示寄存器,可以不管,需要注意的就只有op和BBBB:
op是opcode,就是操作码,例如invoke-virtual的opcode就是0x6E;
而这个BBBB是一个method_id,这个method_id是什么呢?在Dex文件格式中可以看到,Dex的数据中有一个叫做method_ids的列表,这个id就是在表中的索引。而使用这个id呢可以获得这个method的class_id,proto_id和name_id,class_id可以获取到所属的类的信息(class_def_item),proto_id可以获取到方法的参数及返回类型信息(proto_id_item),最后通过string_ids拼凑出一个完整的名称。
具体是这样的:

public String getNameByMethodId(int id) {                return getName(method_id_list.get(id));        }

public String getNameByProtoId(int id) {                return getName(proto_id_list.get(id));        }

public String getName(Proto_Id_Item proto) {                return getString(proto.shorty_id);        }

public String getName(Method_Id_Item method) {                String className = getNameByTypeId(method.class_id).replaceAll("/", "\\.");                className = className.substring(1, className.length() - 2);                return className + "." + getString(method.name_id).replaceAll("\0","") + "("+ getNameByProtoId(method.proto_id).replaceAll("\0","") + ")";        }

public String getString(int id) {                return new String(string_data_list.get(id).body);

那么我们就可以明确了解析任务,解析任务包括class_def_item中所有结构体以及string_ids、string_id_item、string_data_item、proto_ids、proto_id_item、method_ids、method_id_item、type_ids、type_iditem,当然,还有最重要的header。我并不是教大家写代码,所以这个还是靠你们自己干啦,可以参考我的DexParser类以及Format包下的各个类。或者直接找个开源的DexParser项目也是可以直接调用的(话说其实我这个就算是^^)。

贴一个获取全部insns的for:

public ArrayList<encoded_method> getAllEncodedMethod(){                ArrayList<encoded_method> all = new ArrayList<encoded_method>();                for (Class_Def_Item cls : class_def_list) {                        if (cls.class_data == null) {                                continue;                        }                        String clsName = getName(cls);                        all.addAll(cls.class_data.direct_methods);                        all.addAll(cls.class_data.virtual_methods);                }                return all;        }

public ArrayList<insns_item> getAllInsnsItem() {                ArrayList<insns_item> all = new ArrayList<insns_item>();                for (encoded_method method : getAllEncodedMethod()) {                        if (method.code != null) {                                all.addAll(method.code.insns_items);                        }                }                return all;        }//不要问我怎么就这么简单,难道你要我贴一大堆封装的代码出来吗..

其实还有一个比较简单的思路,就是只写一个Code_Item的结构体,然后取出第一个和最后一个encoded_method_item的code_off。然后将这段范围解析为一个CodeItem的List。然后不就可以为所欲为了吗~这样的代码量会相较少很多。主要还是靠自己发挥,我说过我并不教写代码 ^^

这时候关键的两个东西已经有了:获取所有字节码以及从method_id获取名称的方法。那么剩下的就简单了,上面说过invoke指令的格式,知道了invode的opcode后面第二位开始就是一个short的method_id,我们可以从这个id获取到他的名称,然后判断是不是那个加载广告的入口,如果是的话,直接将从opcode开始的6个字节修改为0×00。
示例代码:

DexChanger changer = new DexChanger(new File(path));                DexFile dexfile = changer.getDexFile();                String magiclist[] = {                                "com.google.android.gms.ads.AdView.loadAd",                                "com.google.android.gms.ads.InterstitialAd.loadAd",                                "com.google.android.gms.ads.reward.RewardedVideoAd.loadAd",                                "com.mopub.mobileads.AdViewController.loadAd",                                "com.mopub.mobileads.MoPubInterstitial$MoPubInterstitialView.loadAd"                };                for (insns_item insns : dexfile.getAllInsnsItem()) {                        if (insns.opcode.toString().startsWith("INVOKE")) {

                                changer.move(insns.getFileOff() + 2); // invoke系列指令格式 A|G|op BBBB F|E|D|C ,所以off + 2是methodId                                int methodId = changer.nextShort() & 0xFFFF; // 转为无符号数

                                if (methodId < 0 || methodId > dexfile.getHeader().method_ids_size) { // invoke-custom                                        continue;// 调用的索引有可能是FFFFFE,防止其他意外情况,过滤掉非正常methodId                                }                                String mtd = dexfile.getNameByMethodId(methodId);                                for(String magic : magiclist) {                                        if(mtd.indexOf(magic) != -1) {                                                changer.setNop(insns);                                                System.out.println(insns.getFileOff() + " - invoke method " + mtd);                                        }                                }                        }                }

最后一步就是重建DexHeader,主要就是计算signature和checksum,这个应该不用多说什么:

public void flush() {

super.flush(); // 先将修改的数据flush,否则this.data还是旧数据

DexHeader header = dexFile.getHeader();

try {

this.move(0);

MessageDigest mdTemp = MessageDigest.getInstance(“SHA1″);

mdTemp.update(this.data, 32, this.data.length – 32);

header.signature = mdTemp.digest(); // 计算Signature

System.arraycopy(header.signature, 0, this.data, 12, 20); // 覆盖原Signature

Adler32 checksum = new Adler32();

checksum.update(this.data, 12, this.data.length – 12);

header.checksum = (int) checksum.getValue(); // 计算checksum

} catch (NoSuchAlgorithmException e) {

System.out.println(“[*E]” + “rebuild” + “:” + e.getMessage());

} catch (CursorMoveException e) {

System.out.println(“[*E]” + “rebuild” + “:” + e.getMessage());

}

this.changeData(header.magic);

this.changeInt(header.checksum);

this.changeData(header.signature);

this.changeInt(header.file_size);

this.changeInt(header.header_size);

this.changeInt(header.endian_tag);

this.changeInt(header.link_size);

this.changeInt(header.link_off);

this.changeInt(header.map_off);

this.changeInt(header.string_ids_size);

this.changeInt(header.string_ids_off);

this.changeInt(header.type_ids_size);

this.changeInt(header.type_ids_off);

this.changeInt(header.proto_ids_size);

this.changeInt(header.proto_ids_off);

this.changeInt(header.field_ids_size);

this.changeInt(header.field_ids_off);

this.changeInt(header.method_ids_size);

this.changeInt(header.method_ids_off);

this.changeInt(header.class_defs_size);

this.changeInt(header.class_defs_off);

this.changeInt(header.data_size);

this.changeInt(header.data_off);

super.flush();

}

super.flush()已经包括了写出文件,那么现在,把修改后的dex重新压缩回你的apk里,然后签个名就可以安装跑起来啦~(这个也是可以自动化的,但是我没精力写了,就交给你们吧^_^)。

尾记

如此这般,核心的东西已经有了,后面的部分就请尽情发挥吧。
其实我本来想详细写一下Dex格式的,但是突然懒癌病发,而且关于Dex的资料已经够多了,再有不明白的地方还可以看源码。

最后附上几个去广告成品:http://hluwa.cn/down/

源码地址:https://github.com/Hoimk/DexChange

琢石成器之自动化去广告神器相关推荐

  1. 琢石成器之自动化去广告神器(Green Creator)

    最近发了几个去广告的APK,难道你以为那是我自己干的吗,不不不,你太天真了,这都是自动处理出来的成果 引言 俗话说的好"工欲善其事,必先利其器",在日常工作中,如果拥有一款或N款好 ...

  2. brew 一直等待_去广告神器,一键去除所有主流App广告,看片从此不等待

    求求各位打开今日第2篇文章,忽略内容,直接拉到底部,然后退出,给花卷赏口饭吃,花卷快饿死了,求求你啦!!!! 大家好,今天小花卷为大家带来的是去广告神器,一键去除所有主流App广告,看片从此不等待 . ...

  3. 众人所知,去广告神器:天行广告防火墙!

     电脑中出现广告需要拦截的话,我们可以借助很多的电脑工具来帮助我们完成的 而对于电脑中广告过多无法拦截的情况下,就可能出现很多的问题. 对于日常中的电脑广告则是很普遍的现象,那么需要如何才能够拦截 ...

  4. 去广告神器——李跳跳

    相信不少小伙伴经常被手机app的开屏广告困扰,每次还得自己手动点击跳过,有的时候一不小心点到广告就会跳转到另一个app上(尤其是某多多.某东.某宝),今天推荐个超好用的跳过广告的app--李跳跳. 由 ...

  5. 去广告神器(Adblock Plus离线安装)

    最近实在受不了百度搜索结果里面广告,还有各大网站内嵌的智障广告(CSDN),考虑到某些小伙伴翻墙不方便所以特地提取了crx安装文件,以便离线安装. 1.离线下载 自取:https://pan.baid ...

  6. ADSafe净网大师----所谓的去广告神器竟然在偷偷推送广告

    今天刚开发完的网站上线联调, 偶然发现<head>里多了一个脚本引用: <script async src="http://c.cnzz.com/core.php" ...

  7. 软件:文档下载神器之冰点文库下载器去广告纯净版

    一款网络平台分享文档的下载工具.用户使用冰点文库无需积分就可以自由下载百度.mbalib.豆丁.畅享.max.book118文档,无需注册和登录.并且通过冰点文库下载的文档可以根据用户要求,最终生成高 ...

  8. adguard home上网慢_最新DNS神器AdGuardHome详细配置教程,彻底解决DNS污染问题、去广告、秒开网页!从此网络快人一步...

    好久没有折腾ADgardhome了,随着官方的升级,现在ad home的普及率已经非常高,并且集成在各个路由器固件之中,省去我们自己安装的麻烦.它最大的功能特点就是去广告,当然还有DNS防污染功能,都 ...

  9. CSDN插件限时内测,新用户抢永久免费去广告特权!

    经过程序猿哥哥们和产品小姐姐马不停蹄的疯狂加班,CSDN 官方出品的PC浏览器插件–开发者助手 终于正式上线啦!一键万能操作,新标签页极简个性,让你的浏览器更酷更高效!还有超多实用彩蛋功能等你来解锁! ...

最新文章

  1. 【Android】附加Android源代码Androidandroid_gingerbread_javasrc
  2. 软件验收标准和验收方法_自动门的验收标准
  3. 如何让奇异值分解(SVD)变得不“奇异”?
  4. Linux 实践操作
  5. matlab绘图z=sin(x,用matlab编写函数文件,实现绘制函数 z= sin(x+y)/(x+y)在【-2:2,-2:2]的图像...
  6. 使用二进制的方式安装mysql实践纪要
  7. php 加入日志功能,php怎么写一个日志功能的函数
  8. oneproxy检测主从复制同步延迟
  9. 【bzoj4025】二分图 LCT
  10. 圆弧齿轮啮合原理_图解八种齿轮的加工原理
  11. java sql异常_java.sql.SQLException: Io 异常: Got minus one from a
  12. case when then else end
  13. 农村义务教育经费保障机制改革校长应知应会卡
  14. IIS7.5和IIS8如何设置FTP的pasv端口范围
  15. Spring 事务初始化源码分析
  16. java序列化,看这篇就够了
  17. cad字体修改方案分享-缺少SHX字体、替换字体
  18. 卡内基梅隆计算机金融,卡耐基梅隆大学计算金融项目申请要求是什么?
  19. centos安装git时出错提示make[1]: *** [perl.mak] Error 2 make: *** [perl/perl.mak] Error 2
  20. 一些简单的css,html,js笔记分享给大家,希望能够帮助到大家

热门文章

  1. pytest框架快速入门-pytest运行时参数说明,pytest详解,pytest.ini详解
  2. 我自己很喜欢的几款文案编辑软件
  3. vue项目接入高德地图点击地图获取经纬度及省市区
  4. IOC AOP OOP拙见
  5. PHP源码加密,以前只知道zend guard,今天才知道,原来还有个 ioncube
  6. volice java_Volice~亡者之声~
  7. 2021.2.15-2.22 人工智能行业每周技术动态
  8. 坦克大战java(章程)
  9. [转]从 .NET 开发人员的角度理解 Excel 对象模型
  10. “最不合格”的SAP应聘者: 从大学生到SAP成都研究院开发工程师