我与菜鸟 发表于 2021-3-27 01:36

[广告]静态分析

## 一:前言
前几天碰到的一个APP,因为不太熟悉android开发所以走了不少的弯路,总觉得是在整理一团乱糟糟的毛线,东扯一截西扯一截,终理不清思绪,在知道其中的核心原理之后,又觉得很简单,之前想的看的那么多不过惘然,甚至都不值得记录下来,奈何费了老大劲分析还是决定有始有终的记录下来。个人觉得这个app应该是被别人破解过,其显示广告的逻辑没有了,应该是修改过。
## 二:前置
### 安卓应用代码和网页中javascript的互相调用
#### 1:WebView是什么?
据安卓开发指南中对WebView描述,此类继承自View,算是对其的扩展,用于在安卓应用中进行网页展示,但是仅仅是网页并不包含浏览器中的导航栏和地址栏。
#### 2:如何实现javascript调用android代码?
2.1:先看看开发者指南是如何说的
启用 JavaScript:setJavaScriptEnabled在webview中启用了javascript
```
    WebView myWebView = (WebView) findViewById(R.id.webview);
    WebSettings webSettings = myWebView.getSettings();
    webSettings.setJavaScriptEnabled(true);
```
将JavaScript代码绑定到Android代码:addJavascriptInterface()设置了调用接口,以至于js代码可以调用开发者提供的安卓代码

2.2:结合程序来看,开发者自定义了一个继承自WebView的AdView类,并通过post方式回调了如下代码进行设置。
```
# <ir.adad.client.b>
    public void run() {
      try {
            this.a.getSettings().setJavaScriptEnabled(true); // 在webview中启用javascript
            this.a.h = new r(this.a);
            this.a.addJavascriptInterface(this.a.h, "adad"); // 设置可供js调用的安卓接口,名称是adad(参数二),参数一是接口实例
            if (!(this.a instanceof w)) {
                this.a.a(false, 0, 0, true);
            }
            this.a.loadDataWithBaseURL(o.a().j(), o.a().i(), "text/html", "utf-8", null); //通过HTML字符串加载网址
            m.b("Assigned client to AdView");
      } catch (Exception e) {
            this.a.b = i.NotAssigned;
            this.a.e();
            this.a.j();
            e.printStackTrace();
            m.d("Could not assign downloaded client with error " + e);
      }
    }

```

#### 3:如何实现android代码调用javascript?
![](https://attach.52pojie.cn//forum/202103/27/012055ogvfxcfxcau33qg5.png?l)
上图中对要执行的javascript进行了拼接,最终其会调用loadUrl来执行
```
public void run() {
      d.super.loadUrl(this.a.a); //this.a.a 就是拼接出来要执行的函数
    }

```

## 三:开始
### 1:获取广告逻辑
1.1:先拿到javascript代码
还记得上面加载网页的那个函数吗loadDataWithBaseURL,先看下原型和对应代码
```
# 原型
loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)
# 对应代码
this.a.loadDataWithBaseURL(o.a().j(), o.a().i(), "text/html", "utf-8", null);
```
参数一中o.a().j()最终会调用如下代码:
```
    public String k() {
      String x = aa.x() == null ? "https://js.adad.ir/adad-client/js/" : aa.x();
      if (a) {
            x = c(x);
      }
      try {
            return x + URLEncoder.encode(l(), "utf-8");
      } catch (UnsupportedEncodingException e2) {
            e2.printStackTrace();
            return aa.x();
      }
    }

    public String l() {
      return (((((((((((((((((((BuildConfig.FLAVOR + "?device=" + v.a("DEVICE")) + "&androidid=" + v.a("ANDROID_ID")) + "&token=" + v.a("AdadToken", BuildConfig.FLAVOR, false)) + "&network=" + v.a("NETWORK_CLASS")) + "&data=" + v.a("DATA_CONNECTIVITY")) + "&bazaarversion=" + v.a("BAZAAR_VERSION")) + "&l=" + ae.a().a("content://com.farsitel.bazaar/info/get_uid")) + "&car=" + v.a("NETWORK_OPERATOR")) + "&j=" + y.a().b("content://com.farsitel.bazaar/info/get_jaw")) + "&brand=" + v.a("BRAND")) + "&package=" + v.a("PACKAGE_NAME")) + "&test=" + v.a("AdadTestMode", BuildConfig.FLAVOR, false)) + "&version=" + v.a("VERSION_CODE")) + "&lang=" + v.a("LANGUAGE")) + "&android=" + v.a("ANDROID")) + "&adadversion=" + v.a("ADAD_VERSION")) + "&uid=" + ae.a().b("content://com.farsitel.bazaar/info/get_uid")) + "&model=" + v.a("MODEL")) + "&library=" + v.a("LIBRARY_NAME")) + "&dpi=" + v.a("DPI");
    }
}
```
我们通过上面的url即可拿到HTML源。
打开之后可以看到满屏令人厌恶的命名方式,这里列入出几个比较重要的类名称,其实从名称我们大概可以猜个八九不离十,只不过代码相对庞大一些。
```
AdDelivery      //广告分发相关

BannerAd      //横幅式广告
InterstitialAd//插页式广告
VideoAd         //视频广告
RichAd          //富媒体

Master          //主人...
Slave         //奴隶...
```
1.2 AdDelivery内部逻辑
列举一下重要函数:
```
# 下载
AdDelivery.stopBannerLoop
AdDelivery.startBannerLoop
AdDelivery.getInterstitialAd
AdDelivery.getVideoAd
AdDelivery.getAd
AdDelivery.downloadMedia
AdDelivery.getAdFinished
# 分发
AdDelivery.distributeAd
AdDelivery.giveAdToSlaves

```
大致捋一下内部逻辑:
![](https://attach.52pojie.cn//forum/202103/27/012059dgpz0epao4oo4c3p.png?l)
简述一下逻辑就是根据不同广告类型发送请求并返回一个JSON对象ad
```
ad{
    "adtype":
    "img_url":   
    "base64":
    "video":
    "videoFileName":
    "rich_ad":
    "richImage":
    "logo":
    "richLogo":
    "adId":
    "secret":
    }
```
这里ad['base64']最终被赋值为图片的base64编码
```
AdDelivery.downloadMedia = function (ad, onFail) {
    log("(AdDelivery.downloadMedia) Downloading Media for " + ad.adType);
    switch (ad['adType']) {
      case "text":
      case "banner":
      case "interstitial":
            if (ad['img_url'] != undefined) {
                AdDelivery.getBinary(
                  Utils.generateAdadUrl(ad['img_url']),            //这里会根据ad['img_url']的值下载图片
                  function (result) {
                        ad['base64'] = AdDelivery.base64Encode(result); //这里对请求回来的图片进行base64编码并对ad['base64']进行赋值。
                        AdDelivery.getAdFinished(ad);
                  }, function () {
                        onFail("Couldn't Download Ad Media");
                        logEx("user:error", "Network Error: Check your internet connection please.");
                  });
            } else {
                // nothing to download for text ads without logo
                AdDelivery.getAdFinished(ad);
            }

            break;

      case "video":

```
1.3 回溯
我们以获取插页式广告为例,先查找调用AdDelivery.getInterstitialAd的位置,发现除了自身只有一个地方对其进行了调用。
```
Master.prepareInterstitial = function () {
    if (Master.state.interstitialState == "downloading") {
      logEx("user:error", "Please don't call prepareInterstitial rapidly!");
    }
    else if (Master.state.interstitialState == "ready") {
      logEx("user:error", "Your interstitial ad is ready. you should call showInterstitial()"); //这里的showInterstitial会在显示时候用到
      AdDelivery.getAdFinished(Master.state.currentInterstitialAd);
    }
    else if (Master.state.interstitialState == "showing") {
      logEx("user:error", "Your interstitial ad is being shown. Wait for it to finish.");
    }
    else {
      Master.state.interstitialState = "downloading";// 第一次调用应该是走这里,设置状态为下载中

      if (HostCompat.versionCompare("3.2") >= 0)
            adad.createInterstitial();

      log("Starting interstitial loop ");
      AdDelivery.getInterstitialAd();               //调用位置
    }
};
```
下面就是看哪里调用Master.prepareInterstitial,会发现并没有js代码对其进行了调用,那其实真正的调用实在App代码中。
```
# ir.adad.client.aa
    static void a(InterstitialAdListener interstitialAdListener) {
      c.a("Master.prepareInterstitial()");//这里最后调用loadUrl执行prepareInterstitial,就是前置说的调用机制在起作用
      b = interstitialAdListener;
      q();
    }
```
到这里我们只需要往上追踪ir.adad.client.aa.a的调用堆栈即可。
最终会发现在类com.doaye.imanaminollah.MainActivity.onCreate中被调用,即有启动这个活动的时候就会启动获取插页广告程序。
```
# com.doaye.imanaminollah.Splash (入口类)
public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_splash);
      new Handler().postDelayed(new Runnable() {
            /* class com.doaye.imanaminollah.Splash.AnonymousClass1 */

            public void run() {
                Splash.this.startActivity(new Intent(Splash.this, MainActivity.class)); //这里启动了MainActivity
                Splash.this.finish();
            }
      }, (long) SPLASH_TIME_OUT);
    }
```
到这里大概的广告是如何获取的逻辑就大致捋顺了。

### 2:展示广告逻辑
紧接着上述最后的AdDelivery.distributeAd分发广告继续跟踪下去,仍然是以插页式广告为例。
在开始之前需要先按照自己的理解解释一下Master类中的Master.getSlaves,因为Master.getInterstitialId()调用了他。
```
Master.getSlaves = function (activeOnly) {
    /*
   Result structure:
   {
         count : <int>,
         slaves : {
             id : <long>,
             uiPath : <string>,
             adType : <banner | interstitial>
             clientState:<string>
         }
   }
   */

    var res = activeOnly ?
      JSON.parse(adad.getActiveSlaves()) :    //这里调用了app提供的接口,获取活动的slaves(个人认为是活动的webview视图,每个webview都有自己的id以及对应的类型)
      JSON.parse(adad.getAvailableSlaves());
    var result = {};
    result.slaves = [];

    for (var i = 0; i < res.count; ++i)
      if (res.slaves.clientState == "Started")
            result.slaves.push(res.slaves);

    result.count = result.slaves.length;
    return result;
};
```

```
# AdDelivery.distributeAd
...
else if (ad.adType == "interstitial" && AdDelivery.canModifyInterstitial()) {
      Master.state.currentInterstitialAd = ad;
      Master.state.interstitialState = "ready";       //设置插页式广告准备状态
      var interstitialId = Master.getInterstitialId(); //获取类型是”Interstitial“活动webview
      if(interstitialId.count > 0)
            AdDelivery.giveAdToSlaves(Master.getInterstitialId(), ad);
...

```
紧跟着来看AdDelivery.giveAdToSlaves
```
AdDelivery.giveAdToSlaves = function (slavesToSend, ad) {
    if (ad == undefined) {
      log("(AdDelivery.giveAdToSlaves) No ad yet. AdDelivery will distribute the ad automatically later");
      AdDelivery.startBannerLoop();
    } else {
      var order = Master.createOrder(Constants.CMD_ASSIGN_AD); //这里奴隶主下达了派发广告的命令
      order.ad = Utils.utf8_to_b64(JSON.stringify(ad));      //参数赋值
      Master.orderSelectedSlaves(slavesToSend, order);
    }
};
# order构成,命令必须是有的,紧跟着变量
*
Order is in this format:
order = {
    command : <string>, (This is mandatory)
    param1 : <var>,
    param2 : <var>,
    ...
}
*/
```
接下来执行的顺序如下
```
Master.orderSelectedSlaves ---> (adad.orderSelectedSlaves) ---> 返回来执行Slave.takeOrder系列命令
```
```
Slave.takeOrder = function (orderStr) {
    var order = Utils.decodeIncoming(orderStr);

    if (order == undefined)
      log("(Slave.takerOrder) Order is undefined: " + orderStr);
    else if (order.command == undefined)
      log("(Slave.takeOrder) Order's recipient is not defined: " + orderStr);
    else if (order.command == Constants.CMD_ASSIGN_AD) //之前order的命令是CMD_ASSIGN_AD,所以走这里
      Slave.assignAd(order);
    else if (order.command == Constants.CMD_CALL_LISTENER_FAILED) {
      Slave.callListener("onAdFailedToLoad");
      Slave.resetDimensions();
    } else if(order.command == Constants.CMD_SEND_MESSAGE_TO_HOST)
      Slave.sendMessageToHost({"msg" : order.msg});
    else if (order.command == Constants.CMD_ASSIGN_STATUS)
      Slave.assignStatus(order);
    else
      log("(Slave.takeOrder) Order is not known: " + order.command);
};
```
紧接着看看Slave.assignAd
```
Slave.assignAd = function (order) {
    adad.onAdLoaded();
    currentAd = JSON.parse(Utils.b64_to_utf8(order.ad)); //这里currentAd很重要,后面显示广告参数全由他提供
    Slave.currentRequestId++;
};

```
到这里好像已经结束了,并没有在走下去的逻辑,这时候可能得重新看看是否有显示逻辑了。Master.showInterstitial在上述分析中碰到过,其log提示我们一旦插页式广告准备好了应该调用此函数进行显示,其实Master.showInterstitial本应该实在app代码中调用的,但是通过搜索,发现其最终并没有被调用,所以我怀疑这个APP可能是被修改过的,去掉了广告显示逻辑,但是并不影响我们往下查看,所以我们看看Master.showInterstitial调用了什么?
```
Master.showInterstitial ---> (adad.showInterstitial) ---> 返回来执行Slave.displayAd
```
```
Slave.displayAd = function (order) {
    try {
      Slave.updateHostView();

      if(Slave.isRich()) {
            RichAd.displayAd();

      } else {
            if (currentAd['img_url'] != undefined) {
                var image_format = currentAd['img_url'].split('.').pop();
                if (currentAd['contentType'] == 'text')
                  this.setImageData("ad_logo", image_format, currentAd['base64']);
                else if (currentAd['adType'] == 'interstitial')
                  this.setImageData("interstitial_ad_image", image_format, currentAd['base64']); //插页式广告
                else
                  this.setImageData("ad_image", image_format, currentAd['base64']);
            }
      ...
};

```
当看到上述代码,再看看这个是不是就了然了

![](https://attach.52pojie.cn//forum/202103/27/012102xy265a5wli008166.png?l)

## 四:总结
![](https://attach.52pojie.cn//forum/202103/27/012105ac5tgvtcvx85xs6x.png?l)

我与菜鸟 发表于 2021-3-27 19:17

vosdbk 发表于 2021-3-27 17:27
这广告有点复杂了, 一般的APP不会那么闹腾吧

不知道哎,主要是了解调用结构,具体逻辑可以再分析

我与菜鸟 发表于 2021-3-27 10:25

www2455937517 发表于 2021-3-27 09:59
刚学android开发,看了感觉挺简单的,O(∩_∩)O哈哈~

是的,论会正向开发的重要性:Dweeqw

深入研究学习 发表于 2021-3-27 03:39

写的真好 不愧是大佬{:1_893:}

加奈绘 发表于 2021-3-27 06:25

感谢大佬分享,受益良多

吃兔子的肉 发表于 2021-3-27 07:51

是个狠人,学习了学习了

光咣咣咣丶 发表于 2021-3-27 08:29


感谢大佬分享,受益良多

First丶云心 发表于 2021-3-27 08:37

smmhlove 发表于 2021-3-27 08:47

看了很多,还是看不懂

whngomj 发表于 2021-3-27 09:24

谢谢分享,学习学习

pdcba 发表于 2021-3-27 09:33


谢谢分享,学习学习

www2455937517 发表于 2021-3-27 09:59

刚学android开发,看了感觉挺简单的,O(∩_∩)O哈哈~
页: [1] 2 3 4
查看完整版本: [广告]静态分析