首页
论坛
课程
招聘
[原创]手机毒霸去广告功能分析二:广告View的识别
2013-3-21 10:43 10284

[原创]手机毒霸去广告功能分析二:广告View的识别

2013-3-21 10:43
10284
上次我们提到手机毒霸获得root权限之后,生成一个root进程,然后通过进程注入的方式将广告应用中的广告检测出来,并且清除之。这次我们将详细介绍手机毒霸(v1.6.0)是如何找到广告View的。

一、广告View判定规则分析
接着上次的说,注入代码勾住了ActivityThread中的mH的mCallback回调【@】,拦截了RESUME_ACTIVITY(107)和PAUSE_ACTIVITY(101)消息。直接看dex2jar反编译ksremote.jar后的代码。
@com.ijinshan.remotejar.e
              if (paramMessage.what == 107)
              {
                if (com.ijinshan.hookutil.b.a)
                  Log.d("HookActivityThread", "RESUME_ACTIVITY");
                f.a(paramMessage.obj); //这里就是在Activity resume之前做的事情
                KsRemoteCtrl.a(107);
                break;
              }
              if (paramMessage.what == 101)
              {
                f.b(paramMessage.obj);
                break;
              }

顺藤摸瓜看看f.a(paramMessage.obj);做了些什么。
@ com.ijinshan.remotejar.f
  public static void a(Object paramObject) {//paramObjects实际是IBinder类型的token
    if ((a != null) && (e != null)) {
      Object localObject = a.get(paramObject);//根据token拿到ActivityClientRecord
      if (localObject != null) {
        Activity localActivity = c(localObject);//根据ActivityClientRecord拿到Activity
        if (localActivity != null) {
          //根据Activity拿到com.ijinshan.duba.a.b对象
          com.ijinshan.duba.a.b localb = (com.ijinshan.duba.a.b)e.get(localActivity);
          if (localb != null) localb.d();//接下来要跟这个函数
        }
      }
    }
    d();
  }

其实上面的com.ijinshan.duba.a.b类是一个比较重要的类,它负责检测广告。我们接下去看它的d()方法。
@ com.ijinshan.duba.a.b
  public void d()
  {
    if (com.ijinshan.hookutil.b.a)
      Log.e(a, this.c.toString() + " onResume");
    // com.ijinshan.remotejar.d.b()返回了主线程的一个Handler
    com.ijinshan.remotejar.d.b().postDelayed(this.h, 1000L);
  }

接下来要看这个h是一个什么样的Runnable了。这个成员h是com.ijinshan.duba.a.c类型的。
@com.ijinshan.duba.a.c
  public void run()
  {
    if (!a.h());
    while (true)
    {
      return;
      if (!b.a(this.a).isFinishing())
      {
        b.b(this.a);
        …
        boolean bool = this.a.a(b.a(this.a));//这里如果找到广告View就返回true
        …
        if (bool)
          b.a(this.a, true);
        else if (b.a(this.a).getClass().toString().indexOf("qsbk.app.logic.TopActivity") > -1)
          d.b().postDelayed(b.c(this.a), 500L);
      }
    }
  }

boolean bool = this.a.a(b.a(this.a));这句看上去比较复杂,我们掰开来瞧瞧。
拆成Activity activity = b.a(this.a); boolean bool = this.a.a(activity);
this.a是com.ijinshan.duba.a.b类型。b.a(this.a)的b类也是com.ijinshan.duba.a.b,b.a方法返回一个Activity。this.a.a(activity);就是在这个activity中搜索看是否存在广告View。
@ com.ijinshan.duba.a.b
  public boolean a(Activity paramActivity) {
    int i = 0;
    FrameLayout localFrameLayout = (FrameLayout)paramActivity.getWindow().getDecorView();//获得当前Activity的DecorView
    if (localFrameLayout != null) {
      localFrameLayout.getClass();
      bool = false;
      while (i < localFrameLayout.getChildCount()) {//从根开始遍历所有view
        if (a("---", localFrameLayout, localFrameLayout.getChildAt(i)))//接着看a方法
          bool = true;
        i++;
      }
    }
    boolean bool = false;
    return bool;
  }

这里这个a方法太复杂了,但是非常重要。dex2jar反编译出来的逻辑出现了较大误差,还是用smali吧。
@com/ijinshan/duba/a/b.smali
.method public a(Ljava/lang/String;Landroid/view/View;Landroid/view/View;)Z
    .registers 11
    .parameter
    .parameter
    .parameter

    .prologue
    const/4 v2, 0x0

    const/4 v3, 0x1

    .line 145
    .line 146
  
  #判断当前View是不是ViewGroup
    invoke-virtual {p0, p3}, Lcom/ijinshan/duba/a/b;->a(Landroid/view/View;)Z 

    move-result v0

    if-eqz v0, :cond_b3 
    #若当前View是ViewGroup
    move-object v0, p3

    .line 147
    check-cast v0, Landroid/view/ViewGroup;

    .line 148
  
  #这里恒为1,因为初始化成1后,没有被修改过。
    sget-boolean v1, Lcom/ijinshan/duba/a/b;->b:Z # Lcom/ijinshan/duba/a/b;->b 

    if-eqz v1, :cond_4d

    .line 149
    const/4 v1, 0x2

    new-array v4, v1, [I

    .line 150
  
  #获得当前View在屏幕中的位置
    invoke-virtual {p3, v4}, Landroid/view/View;->getLocationInWindow([I)V 

    .line 151
    const-string v1, ""

    .line 153
    :try_start_17
    invoke-virtual {p3}, Ljava/lang/Object;->toString()Ljava/lang/String;
    :try_end_1a
    .catch Ljava/lang/Exception; {:try_start_17 .. :try_end_1a} :catch_d0

    move-result-object v1

    .line 157
    :goto_1b
  
  #LOG开关,无需太关心
    sget-boolean v5, Lcom/ijinshan/hookutil/b;->a:Z 

    if-eqz v5, :cond_4d

    .line 158
    const-string v5, "recurFindAllView Group"

    new-instance v6, Ljava/lang/StringBuilder;

    invoke-direct {v6}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v6, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v6

    invoke-virtual {v6, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v1

    const-string v6, " pos: "

    invoke-virtual {v1, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v1

    aget v6, v4, v2

    invoke-virtual {v1, v6}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

    move-result-object v1

    const-string v6, " "

    invoke-virtual {v1, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v1

    aget v4, v4, v3

    invoke-virtual {v1, v4}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

    move-result-object v1

    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v1

    invoke-static {v5, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

    .line 160
    :cond_4d
  
  #获得当前ViewGroup的类
    invoke-virtual {p3}, Ljava/lang/Object;->getClass()Ljava/lang/Class; 

    move-result-object v1
  
  #获得当前ViewGroup的类名
    invoke-virtual {v1}, Ljava/lang/Class;->toString()Ljava/lang/String; 

    move-result-object v4

    .line 161
  
  #根据当前ViewGroup的类名判断是不是广告View
    invoke-static {v4}, Lcom/ijinshan/duba/a/a;->b(Ljava/lang/String;)Z 

    move-result v1

    .line 162
    if-eqz v1, :cond_68

  #类名规则命中
    .line 163
    sget-boolean v0, Lcom/ijinshan/hookutil/b;->a:Z

    if-eqz v0, :cond_64

    .line 164
    const-string v0, "AdDetector Group2 LinkIt"

    invoke-static {v0, v4}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

    .line 165
    :cond_64
  #在命中的ViewGroup附近添加adclose View
    invoke-virtual {p0, p3}, Lcom/ijinshan/duba/a/b;->d(Landroid/view/View;)V

    .line 209
    :goto_67
    return v3 # 返回 true

    .line 169
    :cond_68 
  #类名规则未命中,尝试WebView规则
    const-string v1, "android.webkit.WebView"

    invoke-virtual {v4, v1}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z

    move-result v1

    if-eqz v1, :cond_81

    .line 170
    instance-of v1, p3, Landroid/webkit/WebView;

    if-eqz v1, :cond_81

    move-object v1, p3

    .line 171
    check-cast v1, Landroid/webkit/WebView;

  #判断这个WebView是不是广告View
    invoke-direct {p0, v1}, Lcom/ijinshan/duba/a/b;->b(Landroid/webkit/WebView;)Z 

    move-result v1

    if-eqz v1, :cond_81

  #WebView规则命中
    .line 172
  #在命中的ViewGroup附近添加adclose View
    invoke-virtual {p0, p3}, Lcom/ijinshan/duba/a/b;->d(Landroid/view/View;)V

    goto :goto_67

    .line 178
    :cond_81
  #WebView规则未命中
    const-string v1, "com.android.internal.policy.impl.PhoneWindow"

  #排除规则:判断当前ViewGroup的类名是否包含"com.android.internal.policy.impl.PhoneWindow"
    invoke-virtual {v4, v1}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z

    move-result v1

    if-eqz v1, :cond_8b

  #即将return false
    move v3, v2

    .line 179
    goto :goto_67

    :cond_8b
    move v1, v2

    .line 181
    :goto_8c
  # 遍历当前ViewGroup的子View,递归调用本函数
    invoke-virtual {v0}, Landroid/view/ViewGroup;->getChildCount()I

    move-result v4

    if-ge v1, v4, :cond_ce

    .line 182
    invoke-virtual {v0, v1}, Landroid/view/ViewGroup;->getChildAt(I)Landroid/view/View;

    move-result-object v4

    .line 183
    new-instance v5, Ljava/lang/StringBuilder;

    invoke-direct {v5}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v5, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v5

    const-string v6, "---"

    invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v5

    invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v5

    invoke-virtual {p0, v5, v0, v4}, Lcom/ijinshan/duba/a/b;->a(Ljava/lang/String;Landroid/view/View;Landroid/view/View;)Z #递归

    move-result v4

    if-eqz v4, :cond_b0 #即使已经命中某个子View,依然搜索其他子View

    move v2, v3

    .line 181
    :cond_b0
    add-int/lit8 v1, v1, 0x1

    goto :goto_8c

    .line 189
    :cond_b3 
  # 当前View不是ViewGroup
    invoke-virtual {p3}, Ljava/lang/Object;->getClass()Ljava/lang/Class;

    move-result-object v0

  #取到当前View的类名
    invoke-virtual {v0}, Ljava/lang/Class;->toString()Ljava/lang/String; 

    move-result-object v0

    .line 190
  #判断当前View的类名是否是广告View,是返回true,否则返回false
    invoke-static {v0}, Lcom/ijinshan/duba/a/a;->b(Ljava/lang/String;)Z 

    move-result v1

    .line 191
    if-eqz v1, :cond_ce 
  #类名规则命中

    .line 192
    sget-boolean v1, Lcom/ijinshan/hookutil/b;->a:Z

    if-eqz v1, :cond_ca

    .line 193
    const-string v1, "AdDetector Group2 LinkIt"

    invoke-static {v1, v0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

    .line 194
    :cond_ca
  #在命中的View附近添加adclose View
    invoke-virtual {p0, p3}, Lcom/ijinshan/duba/a/b;->d(Landroid/view/View;)V

    goto :goto_67

    :cond_ce
  #类名规则未命中
    move v3, v2 

    .line 209
    goto :goto_67

    .line 155
    :catch_d0
    move-exception v5

    goto/16 :goto_1b
.end method

对这个a()函数做一下总结:整个广告匹配过程中一共使用了3种规则:
1.  类名规则:当前View对象的类名如果与黑名单中的类名匹配,那么就认定为广告。
2.  WebView规则:当前View对象是WebView并且满足一定的规则,那么就认定为广告。
3.  排除规则:当ViewGroup对象的类名如含有” com.android.internal.policy.impl.PhoneWindow”, 那么就肯定不是广告。
a()函数的算法大体如下:
首先判断当前View是不是ViewGroup。
1.  非ViewGroup情况下,只用类名规则匹配
2.  ViewGroup情况下,以此用类名规则和WebView规则匹配,如果还未匹配上,则用排除规则排除PhoneWindow的情况。接下来就是遍历当前ViewGroup的所有子View,然后递归调用a()。
一旦出现命中,则广告View的位置上将被加上adclose View。这个adclose View就是给用户点击来移除广告用的。

以下给出类名规则和WebView规则的细节。(排除规则上述代码中已有)
类名规则:
@com.ijinshan.duba.a.a
  public static boolean b(String paramString)
  {
    if (paramString.startsWith("class "))
      paramString = paramString.substring(6); //将View的类名移除开头”class ”
    boolean bool;

//过滤掉肯定不是广告的类名。请看下面a(paramString.getBytes()函数的具体实现。
    if (a(paramString.getBytes()))【@】
      bool = false;
    while (true)
    {
      return bool;
      synchronized (f)
      {
        if ((j == null) || (j.size() == 0))
        {
          bool = false;
          continue;
        }
        if (TextUtils.isEmpty(paramString))
        {
          bool = false;
          continue;
        }
        String[] arrayOfString = d.a(paramString, ".");//将View的类名按照”.”拆分成字符串数组
        if (arrayOfString.length >= 2)//必须要有包名。【@】
        {
          String str1 = arrayOfString[0] + "." + arrayOfString[1];

          //这里j就是广告View的类名黑名单,它是一个二级Map。
          List localList = (List)j.get(str1);
          if ((localList != null) && (!localList.isEmpty()))
          {
            Iterator localIterator = localList.iterator();
            while (true)
              if (localIterator.hasNext())
              {
                String str2 = (String)localIterator.next();
                if (paramString.contains(str2)) //匹配上了。命中!
                {
                  if (b.a)
                    Log.e("ad", "IsAdViewCls clsSign " + str2 + " || clsName " + paramString + " return true");
                  bool = true;
                  break;
                }
              }
          }
        }
        bool = false;
      }
    }
  }

总的来说就两步:先过滤掉一些金山认为肯定不含广告的包名,然后到黑名单中去匹配。至于这个黑名单怎么来的。这里我不想展开了。这里只想简单的说这个黑名单是通过IPC从手机毒霸的主进程哪里获得的。至于哪些包名是金山认为肯定不含广告的包名呢?请大家看下面的函数。

@com.ijinshan.duba.a.a
//过滤掉肯定不是广告的类名。
  public static boolean a(byte[] paramArrayOfByte)
  {
    boolean bool = true;
    if ((paramArrayOfByte.length >= 6) && (paramArrayOfByte[0] == 106) && (paramArrayOfByte[bool] == 97) && (paramArrayOfByte[2] == 118) && (paramArrayOfByte[3] == 97) && (paramArrayOfByte[4] == 46) && (paramArrayOfByte[5] == 108));
    while (true)
    {
      return bool;
      if (((paramArrayOfByte.length < 7) || (paramArrayOfByte[0] != 100) || (paramArrayOfByte[bool] != 97) || (paramArrayOfByte[2] != 108) || (paramArrayOfByte[3] != 118) || (paramArrayOfByte[4] != 105) || (paramArrayOfByte[5] != 107) || (paramArrayOfByte[6] != 46)) && ((paramArrayOfByte.length < 8) || (paramArrayOfByte[0] != 97) || (paramArrayOfByte[bool] != 110) || (paramArrayOfByte[2] != 100) || (paramArrayOfByte[3] != 114) || (paramArrayOfByte[4] != 111) || (paramArrayOfByte[5] != 105) || (paramArrayOfByte[6] != 100) || (paramArrayOfByte[7] != 46) || ((paramArrayOfByte.length >= 15) && (paramArrayOfByte[8] == 99) && (paramArrayOfByte[9] == 111) && (paramArrayOfByte[10] == 109) && (paramArrayOfByte[11] == 109) && (paramArrayOfByte[12] == 111) && (paramArrayOfByte[13] == 110) && (paramArrayOfByte[14] == 46))) && ((paramArrayOfByte.length < 13) || (paramArrayOfByte[0] != 99) || (paramArrayOfByte[bool] != 111) || (paramArrayOfByte[2] != 109) || (paramArrayOfByte[3] != 46) || (paramArrayOfByte[4] != 97) || (paramArrayOfByte[5] != 110) || (paramArrayOfByte[6] != 100) || (paramArrayOfByte[7] != 114) || (paramArrayOfByte[8] != 111) || (paramArrayOfByte[9] != 105) || (paramArrayOfByte[10] != 100) || (paramArrayOfByte[11] != 46) || (paramArrayOfByte[12] != 114)) && ((paramArrayOfByte.length < 13) || (paramArrayOfByte[0] != 99) || (paramArrayOfByte[bool] != 111) || (paramArrayOfByte[2] != 109) || (paramArrayOfByte[3] != 46) || (paramArrayOfByte[4] != 105) || (paramArrayOfByte[5] != 106) || (paramArrayOfByte[6] != 105) || (paramArrayOfByte[7] != 110) || (paramArrayOfByte[8] != 115) || (paramArrayOfByte[9] != 104) || (paramArrayOfByte[10] != 97) || (paramArrayOfByte[11] != 110) || (paramArrayOfByte[12] != 46)))
        bool = false;
    }
  }

是不是觉得有点恶心?dex2jar反编译也有点问题。不过仔细整理一下,其实逻辑很简单:
"java.l"打头的类名、“dalvik.”打头的类名、“android.”打头但不是 “androd.common.”打头的类名、“com.android.r”打头的类名和“com.ijinshan.”打头的类名都不会被认为是广告。

WebView规则:
@com.ijinshan.duba.a.b
  private boolean b(WebView paramWebView)
  {
    Object localObject = a(paramWebView); //调用下面的函数
    if ((com.ijinshan.hookutil.b.a) && (localObject != null))
      Log.e(a, "getWebViewClient: " + localObject.toString());
    //判断WebViewClient对象类名
//这里貌似是特别针对万普世纪(com.waps)的广告。
    if ((localObject != null) && (localObject.getClass().toString().contains("com.waps")));
    for (boolean bool = true; ; bool = false)
      return bool;
  }

  public Object a(WebView paramWebView)
  {
    try
    {
      //反射得到当前WebView的getWebViewClient方法。
      Method localMethod = Class.forName(KSCONST.decrypt("android.webkit.WebView")).getDeclaredMethod(KSCONST.decrypt("getWebViewClient"), new Class[0]);
      localMethod.setAccessible(true);
      //调用getWebViewClient()
      Object localObject2 = localMethod.invoke(paramWebView, new Object[0]);
      localObject1 = localObject2;
      return localObject1;
    }
    catch (Exception localException)
    {
      while (true)
      {
        localException.printStackTrace();
        Object localObject1 = null;
      }
    }
  }

二、总结
经过了上面的介绍,我相信大家基本上对于手机毒霸是如何判定一个View是广告的原理已经有了较清楚的了解。总的来说就是通过View的类名匹配。手机毒霸存有一个黑名单。如果一个View的类名跟这个黑名单匹配上,那么这个View就被认作是广告View。寻找广告View的过程则是从Activity的根View上树形遍历所有的View。

三、如何躲避手机毒霸的查杀
细心的读者可能已经发现了,我在文中加了三处【@】。这三处也是躲避手机毒霸查杀的入口点。
1.  既然"java.l"打头的类名、“dalvik.”打头的类名、“android.”打头但不是 “androd.common.”打头的类名、“com.android.r”打头的类名和“com.ijinshan.”打头的类名都不会被认为是广告。那么如果一个广告SDK的View的package名就以这些打头手机毒霸不就放过去了么。
2.  既然一个View的类名必须含有包名,那么如果将广告View写在默认包里不就可以躲过去了么。因为arrayOfString的长度必须要大于等于2才行。
String[] arrayOfString = d.a(paramString, ".");//将View的类名按照”.”拆分成字符串数组

3.  这点放在最后说,是因为相对有点工作量,并且还需要对我们的上一篇《手机毒霸去广告功能分析一:总体分析》有一个比较详尽的了解。广告View之所以被检测到是因为手机毒霸Hook了AndroidThread.mH的mCallback。广告SDK可以在每次显示广告前检查mCallback有没有被修改,如果发现mCallback被修改了,把它改回去就可以了。这样就从根本上使的手机毒霸的HOOK失效了。广告拦截更加无从谈起。

手机毒霸去广告功能分析一:总体分析
手机毒霸去广告功能分析二:广告View的识别
手机毒霸去广告功能分析三:java代码(dex)注入

@安卓安全小分队
http://weibo.com/androidsecurity

[注意] 招人!base上海,课程运营、市场多个坑位等你投递!

收藏
点赞0
打赏
分享
最新回复 (6)
雪    币: 5434
活跃值: 活跃值 (163)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Protected 活跃值 2013-3-21 10:49
2
0
拜读中,沙发......
雪    币: 540
活跃值: 活跃值 (239)
能力值: ( LV12,RANK:320 )
在线值:
发帖
回帖
粉丝
evilkis 活跃值 7 2013-3-21 11:07
3
0
反编译后都是源码.....
雪    币: 202
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
无胜 活跃值 2013-4-9 20:24
4
0
我也想学习,但是感觉有点难~~~
雪    币: 6817
活跃值: 活跃值 (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
聖blue 活跃值 2017-2-16 20:36
5
0
支持下!!!!!
雪    币: 35
活跃值: 活跃值 (19)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wyhacket 活跃值 2017-2-16 23:56
6
0
回帖支持,拜读受用
雪    币: 0
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Dvive 活跃值 2017-2-17 13:49
7
0
比较66666比较66666
游客
登录 | 注册 方可回帖
返回