Android 第三方应用广告拦截实现


前年在的上一家公司,制造的机器里应用装有不良广告,严重影响了儿童客户使用者的思想健康,导致被人投诉。于是乎,就有了想研发一款类似于360广告屏蔽的应用的念头。嗯,事情就是这样,现在切入主题。

目前市场上有很多安全软件,它们拦截第三方应用广告的方式都不一样,比如说有 以so 注入方式来拦截弹出广告
现在我们来看下这种方式的详细情况:

要做到拦截,首先我们得知道广告是怎么出来的,原来第三方应用大部分是以加入广告jar形式加入广告插件,然后在AndroidManifest中声明广告service或者在程序中执行广告Api,广告插件再通过Http请求去加载广告。在java中,有四种访问网络的接口,如apache的http库(如下介绍),这几种方式首先都会通过getaddrinfo函数获取域名地址,然后通过connect函数连接到服务器读取广告信息。

  1. WebView(源码文件在frameworks/base/core/java/android/webkit/WebView.java)。通过WebView类的void loadUrl(String url)、void postUrl(String url, byte[] postData)、void loadData(String data, String mimeType, String encoding)、void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)、void evaluateJavascript(String script, ValueCallback resultCallback)等加载网页。
  2. apache-http(源码目录在external/apache-http/ , HttpGet 和 HttpPost类)。通过external/apache-http/src/org/apache/http/impl/client/DefaultRequestDirector.java中的DefaultRequestDirector类的HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)方法执行访问的网络的动作。
  3. okhttp(源码目录在external/okhttp/)。通过external/okhttp/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java中的HttpEngine类的private void connect(Request request) throws IOException方法连接网络。
  4. URL(源码在libcore/luni/src/main/java/java/net/URL.java)。通过libcore/luni/src/main/java/java/net/URL.java中的URL类的URLConnection openConnection() throws IOException方法和URLConnection openConnection(Proxy proxy) throws IOException方法连接网络。

然后再来说说动态库注入,具体什么是动态库注入,以及如何注入,网上有很多文章,这里就不介绍。动态库注入拦截呢,主要是拦截getaddrinfo,根据条件返回错误URL(源码在libcore/luni/src/main/java/java/net/URL.java)。通过libcore/luni/src/main/java/java/net/URL.java中的URL类的URLConnection openConnection() throws IOException方法和URLConnection openConnection(Proxy proxy) throws IOException方法连接网络。拦截网络请求,达到拦截作用。不过需要注意一点就是拦截之前要确定你所拦截的动态库是否是你需要拦截的库?例如A程序调用了动态库BO和CO,而BO和CO都调用了connect函数,此时需要拦截BO的请求,需要注入到BO动态库并修改GOT表,而不是注入到CO中。

拦截HTTP方式广告在多数广告包中,应用程序首先会通过apache的http库或JDK中的http方法先将广告数据下载过来,然后通过WebView显示。这种方式通过注入拦截进程的/system/lib/libjavacore.so可实现广告地址拦截。
拦截WebView方式广告广告插件也可以直接通过WebView加载URL,通过分析WebView加载流程可知它的网络处理过程均交给libchromium_net库来完成。因此通过注入libjavacore.so是无法实现拦截,而是需要注入到/system/lib/libchromium_net.so。

通过这种方式已经完全能够拦截掉第三方APP广告,但存在一些问题:
1.广告商可以通过JNI方式调用系统getaddrinfo与connect实现自己的解析与连接过程的动态库,从而跳过libjavacore.so导致拦截无效。 2.拦截WebView方式广告虽然能够不显示广告,但通常仍然会有浮动框显示”网页无法打开”,从而影响美观。 3.最重要的是我们机器是没有root权限的!!
第三个问题直接导致了放弃了这种注入做法。 来来去去一段时间后,目前是采用android 系统本地扫描第三方应用广告形式。具体怎么做,请往下看!

如果对这种方式不了解的话,建议先看下这篇 Android系统扫描带广告应用的做法

所以具体广告插件扫描方案是匹配包名+类名形式的: 1.扫描本地所有第三方应用,列出一个应用中的所有类,将包名+类名方式与广告插件特征库进行匹配2.将匹配出来的应用所带广告特征,通过系统提供传入接口,将这些规则设置进去。(当然,系统代码是需要改的,做了一些处理,主要是在上面介绍中的几种访问网络方式上做了判断处理)
这种方案的关键在于广告特征库的完善,广告插件特征库收集越全,扫描出来的广告插件就可以越准确。所幸,公司有几位大神,做过类似的事情,所以工作简单了多些。
获取第三方应用:

/** * 查询机器内非本公司应用 */ 
public List<PackageInfo> getAllLocalInstalledApps() { 
      List<PackageInfo> apps = new ArrayList<PackageInfo>(); 
      if(pManager == null){ 
            return apps; 
      }
      //获取所有应用  
      List<PackageInfo> paklist = pManager.getInstalledPackages(0);
      for (int i = 0; i < paklist.size(); i++) { 
           PackageInfo pak = (PackageInfo) paklist.get(i); 
           //屏蔽掉公司内部应用
           //...
           //判断是否为非系统预装的应用程序 
           if ((pak.applicationInfo.flags & pak.applicationInfo.FLAG_SYSTEM) <= 0) { 
               // customs applications  apps.add(pak); 
         } 
     } 
     return apps; 
}

============================
获取某个应用的广告特征

public static List<String> getClassNameByDex(Context context, String packageName) { 
          List<String> datalist = new ArrayList<String>(); 
          String path = null; 
          try { 
               path = context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir;
          // 获得某个程序的APK路径 
          } catch (NameNotFoundException e) { 
                e.printStackTrace(); 
          }
          try { 
               if(TextUtils.isEmpty(path)){ 
                    return datalist;
               } 
               DexFile dexFile = new DexFile(path);// get dex file of APK  
               Enumeration<String> entries = dexFile.entries(); 
               while (entries.hasMoreElements()) {// travel all classes 
                     String className = (String) entries.nextElement(); 
                     String totalname = packageName + "."+className;          
                     datalist.add(totalname); 
               } 
           } catch (IOException e) { 
                 e.printStackTrace(); 
           } 
           return datalist; 
 }

=======================================
将应用中的所有类名与特征库进行匹配:


for (PackageInfo info : infolsit) { 
       if (info == null) { 
             continue; 
       } 
       data = getClassNameByDex(context,info.packageName);
       if(data == null){ 
            Log.d(TAG,"getAdFlagForLocalApp() 类名解析出错"+info.packageName); 
            continue; 
       } 
       sgPgmap = new HashMap<String, String>(); 
       for (String clsname : data) { 
            for (ADSInfo adinfo : flaglist) { 
                   String flag = adinfo.getAdFlag(); //广告样本库的某一标识  
                   String adpg = adinfo.getAdName(); //广告样本库的某一包名 
                   if (clsname.contains(adpg)) { //匹配类名与广告特征库里的匹配符,看是否包含关系 
                       sgPgmap.put(flag,info.packageName); 
                   } 
            } 
       } 
       if(sgPgmap.size() > 0){ //AdsPgInfo 一个对应应用里包含了多少个标识 
             adspginfo = new AdsPgInfo(info.packageName, sgPgmap);      
             pglist.add(adspginfo); 
       } 
}

ps: 在匹配时,有一个很注意的点,有时候单单类名匹配不准,或者会漏掉某些广告,所以应该加上包名,再去匹配特征库里的匹配符,这样才能百无一漏。
在此举例一个指智广告的特征(特征显示形式可自定义,只要符合自己的解析策略即可):

ads.banner.zhidian#指智广告#com/adzhidian/#ad.zhidian3g.cn

ads.banner.zhidian 为该类型广告标识,主要是为了匹配时应用对应标识的简洁性,不用直接跟着一群特征到处跑。。
指智广告 该广告名称
com/adzhidian/ 该广告用来匹配应用中类名的匹配符,当应用中某一(包名+类名)包含该匹配符时,说明了该应用包含该广告
ad.zhidian3g.cn 需要传给系统的一个规则特征。

匹配出所有应用的所属规则特征后,接下来需要传给系统了,系统将满足需求的几个接口提供出来。这边涉及到修改系统层代码,我就主要讲下实现思路,会贴出关键的几个代码。 实现思路:系统根据应用层传入的应用包名以及规则,将其缓存,在webview或http处请求时,对其进行判断处理。
添加某应用规则接

/** * add Adblock url of package pkgName */ 
private boolean addAdblockUrlInner(String pkgName, String url) {
       synchronized (mAdblockEntries) { 
              HashMap<String, UrlEntry> pkgEntry = mAdblockEntries.get(pkgName); 
              if (pkgEntry == null) { 
                    pkgEntry = new HashMap<String, UrlEntry>(); 
                    if (pkgEntry == null) { 
                         Slog.e(TAG, "addAdblockUrl():new HashMap<String, UrlEntry>() fail!");
                         return false; 
                    } 
                    mAdblockEntries.put(pkgName, pkgEntry); 
               } 
               UrlEntry entry = pkgEntry.get(url); 
               if (entry == null) { 
                    pkgEntry.put(url, new UrlEntry(0, false)); 
               } else { 
                    entry.deleted = false; 
               } 
          } 
          return true;
  }

==============================
WebView类postUrl处判断处理

/** 
 * Loads the given URL. * * @param url the URL of the resource to load 
 */
public void loadUrl(String url) { 
       checkThread(); 
       if (!isAddressable(url)) { 
              return; 
       } 
       if (DebugFlags.TRACE_API) 
              Log.d(LOGTAG, "loadUrl=" + url); 
       if(!isChromium && url.startsWith("file://")){ 
              Log.e("WebView.java", "loadurl setLocalSWFMode"); 
       mProvider.setLocalSWFMode(); 
 } 
/** 
 * Returns true if the url is not included by adblock service 
 */ 
private boolean isAddressable(String url) { 
     boolean addressable = true; 
     AdblockManager adblockManager = AdblockManager.getInstance(); 
     if (adblockManager != null) { 
          String adblockUrl = adblockManager.containedAdblockUrl(ActivityThread.currentPackageName(), url); 
          if (adblockUrl != null) { 
                 addressable = false;        
                 adblockManager.increaseNumberOfTimes(ActivityThread.currentPackageName(), adblockUrl); 
          } 
      } 
      return addressable;
}

由于系统代码这部分的改动并非是我改的,更深细节处的理论就不清楚了。 应用层的广告特征库为了可以持续更新,建议可以做成网络更新方式。 据此,广告拦截功能实现就完成了,可能会有瑕疵,不过持续优化中。

如果觉得此文不错,麻烦帮我点下“喜欢”。么么哒!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容