首页
社区
课程
招聘
[原创]某艺TV版 apk 破解去广告及源码分析
2023-4-7 12:50 64716

[原创]某艺TV版 apk 破解去广告及源码分析

2023-4-7 12:50
64716

某艺TV版 apk 破解去广告及源码分析

目录

1. 准备工作

用例 APK

 

用例 APK 版本:爱奇艺TV版 v12.1.0

 

原 APK 链接:https://pan.baidu.com/s/1zNKk662TvoNoSA8NFSWdTQ?pwd=0esa

 

修改 APK 链接:https://pan.baidu.com/s/16xo6FN_o284lAUNThlpdrQ?pwd=94jt

 

本人测试环境

  1. 测试机 Pixel 2(Android 10)
  2. frida v15.1.27(objection 插件 v1.11.0)
  3. 开发者助手 v1.2.1
  4. MT管理器 v13.3
  5. jadx-gui v1.4.4
  6. fiddler v5.0.20211.51073

2. 去除广告思路

2.1 Android 广告

2.1.1 Android 中的广告形式

广告的表现形式很多,可能是一个界面(activity),可能是局部在上方或下方的一个区域视图(view)等。以下是常见广告形式:

  1. 嵌入式广告:将广告直接嵌入到应用程序中,通常出现在应用程序的底部、顶部或侧边栏。
  2. 插页式广告:应用程序的某个时间点弹出的广告,通常会覆盖整个屏幕。插页式广告通常在应用程序的特定事件之后出现,例如游戏中的关卡结束或应用程序的主菜单页面。
  3. 横幅广告:在应用程序的顶部或底部显示的广告,通常以图像或文本的形式出现。横幅广告通常比嵌入式广告小,不会占用应用程序的太多空间。
  4. 视频广告:在应用程序中播放的广告,通常以全屏或插页式的形式出现。通常需要观看一段时间才能跳过或关闭。

2.1.2 Android 广告来源

  1. Push 推送广告:通过推送消息到用户设备通知栏上展示广告。
  2. 第三方 SDK 广告:很多应用都会集成第三方广告平台,比如 AdMob、Facebook Audience Network、Unity Ads 等等,应用程序可以用第三方广告 SDK 来从其他公司的广告库中获取广告并在应用程序中展示。

2.2 Android 去除广告思路

无论怎样形式、怎样来源的广告,在本地一定需要展示出来,展示就需要广告内容载体,如界面、视图等,对于这些容器,即可以利用静态的布局,也可以动态生成布局。如果能移除这些容器、或者破坏容器生成条件就可以达到去广告的地步。

  • 对于静态布局的广告:广告图片视频都是保存在apk里的,只需要直接从配置清单 xml 文件,或相应的布局xml文件入手,修改容器的布局或者删除相应的代码,就可去除广告。
  • 对于动态导入第三方SDK的广告:我们就需要从代码逻辑上入手。找到它动态导入广告的地方,尝试修改判断条件,从而使导入广告失败,或者让广告无法显示,从而去除广告。

 

本次案例是来自于第三方 SDK 软件的广告投放,通过发送请求包,从而获取相对应的广告 ID 与资源,对于这种情况,我们可以通过定位 SDK 的初始化、广告请求、广告展示等代码,来分析其逻辑,从而找到突破点。

3. 分析开屏广告

3.1 分析步骤

3.1.1 分析广告页面

 

首先对开屏广告页面进行分析,通过MT管理器发现该广告是处在 WelcomeActivity 类中,我们直接hook 类,得到其函数调用栈。

 

3.1.2 分析启动时函数调用栈

可以猜测 showHomePage() 就是展示我们的主页了,我们逐条分析广告发生前的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
private void checkPermission() {
    if (lpt2.br(InitHelper.getInstance().checkInitPermission(this))) {
        jumpToMain();
        return;
    }
    List<String> checkInitPermission = InitHelper.getInstance().checkInitPermission(this);
    androidx.core.app.aux.a(this, (String[]) checkInitPermission.toArray(new String[checkInitPermission.size()]), 1);
}
 
// 检查初始化权限
public List<String> checkInitPermission(Context context) {
    ArrayList<String> arrayList = new ArrayList();
    ArrayList arrayList2 = new ArrayList();
    arrayList.add("android.permission.INTERNET");   // 访问网络的权限
    if (!org.qiyi.speaker.u.con.bMX()) {
        arrayList.add("android.permission.READ_PHONE_STATE");    // 取手机状态的权限
    }
    arrayList.add("android.permission.WRITE_EXTERNAL_STORAGE");   // 写入外部存储设备的权限
    arrayList.add("android.permission.ACCESS_NETWORK_STATE");    // 访问网络状态的权限
    ....
}
 
private void jumpToMain() {
    Log.e("gzy", "size:" + SpeakerApplication.getInstance().getCurrentActivitySize());
        // 用户是否给软件授权
    if (!org.qiyi.speaker.o.con.bLa()) {
        org.qiyi.speaker.o.con.a(this, this.mLisenceCallback);  // 显示免责声明并进行用户许可
        // 加载splash启动页动画(没有后台进程)
    } else if (GuideController.INSTANCE.needShowSplashGuide()) {
        showGuidePage();
    } else {
                //
        launchMain(false);
    }
}
 
// 首次打开,启动应用程序主界面
public void launchMain(final boolean z) {
        // 如果当前Activity数量不等于1,那么显示主页。
    if (SpeakerApplication.getInstance().getCurrentActivitySize() != 1) {
        showHomePage(z);
        return;
    }
        // 注册一个启动画面的回调,请求广告并下载,当启动画面结束后, 显示广告。
    com.qiyi.video.g.con.aXh().registerSplashCallback(new ISplashCallback() { // from class: com.qiyi.video.speaker.activity.WelcomeActivity.2
        @Override // org.qiyi.video.module.api.ISplashCallback
        public void onAdAnimationStarted() {
        }
 
        @Override // org.qiyi.video.module.api.ISplashCallback
        public void onAdCountdown(int i) {
        }
 
        @Override // org.qiyi.video.module.api.ISplashCallback
        public void onAdOpenDetailVideo() {
        }
 
        @Override // org.qiyi.video.module.api.ISplashCallback
        public void onAdStarted(String str) {
        }
 
        @Override // org.qiyi.video.module.api.ISplashCallback
        public void onSplashFinished(int i) {
            WelcomeActivity.this.showHomePage(z);
            JobManagerUtils.a(new Runnable() { // from class: com.qiyi.video.speaker.activity.WelcomeActivity.2.1
                @Override // java.lang.Runnable
                public void run() {
                    com.qiyi.video.qysplashscreen.ad.aux.aUv().aUE();
                    ((ISplashScreenApi) ModuleManager.getModule(IModuleConstants.MODULE_NAME_SPLASH_SCREEN, ISplashScreenApi.class)).requestAdAndDownload();
                }
            }, 500, PageAutoScrollUtils.HANDLER_SWITCH_NEXT_TIPS_DELAY, "splashAD_requestad", WelcomeActivity.TAG);
        }
    });
    launchAppGuide();
}

3.1.3 修改 if 判断

可以看到当当前Activity数量不等于1时,就直接调 showHomePage 函数,我们可以将这个判断改为永真,让其直接显示主页。

 

 

重打包编译签名,运行程序,已去除开屏广告:

 

3.2 总结

对于开屏广告,我们可以观察应用启动的 Acitivity 顺序 (先从主入口切入Main),寻找其函数调用顺序,找到其播送广告的页面,将其逻辑更改,就可以屏蔽掉开屏广告。

4. 分析播放视频广告

4.1 分析步骤

4.1.1 分析广告页面

 

首先对视频广告页面进行分析,有暂停键、静音键、详情键、持续时间、会员关闭提示…,我们可以想到:

  • 剩余时间:获取广告时长,并设置计时器(可能会有判断时间归零,结束视频)
  • 了解详情:获取广告 ID,设置按钮监听,保存广告详情 url
  • 暂停键:保留当前广告播放位置

……

4.1.2 分析持续时间

本人选择剩余时间作为破解入口,通过开发者助手查到显示时间的资源 ID 是 R.id.account_ads_time_pre_ad,搜索资源ID可得三处引用该资源。

 


 

通过 hook 分析发现在视频启动时的广告,调用的是 aux 类的函数:

 

 

分析 aux 类里使用了R.id.account_ads_time_pre_ad 的方法,找到三处,分别分析:

 

第一、二处均用在 Xi() 函数中,该函数主要设置广告配置及布置广告界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private void Vz() {
        ......
        this.bPB = (TextView) findViewById(R.id.account_ads_time_pre_ad);
}
 
private void Xi() {
    ...
    // 获取当前广告播放器的状态
    BaseState currentState = this.mAdInvoker.getCurrentState();    
    // 获取了广告播放器的UI策略
    int adUIStrategy = this.mAdInvoker.getAdUIStrategy();      
    // 打印日志
    com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_ROLL", "{GPhoneRollAdView}", " show ad UI, current state = ", currentState, ", adUiStrategy: ", Integer.valueOf(adUIStrategy));
    // 设置视图的背景,根据当前广告播放器的状态来选择不同的背景资源
    this.bPy.setBackgroundResource(currentState.isOnPaused() ? R.drawable.qiyi_sdk_play_ads_player : R.drawable.qiyi_sdk_play_ads_pause);
    // 获取了当前广告的交付类型
    int i = this.mDeliverType;
    boolean z = i == 3 || i == 7 || i == 4;
    // 获取广告播放器配置
    QYPlayerADConfig adConfig = this.mAdInvoker.getAdConfig();
    int i2 = 8;
    // 根据UI策略的不同值,来设置一些视图的可见性或执行一些方法,8不可见,0可见
    if (adUIStrategy == 1) {
        this.bPA.setVisibility(8);
        this.bPy.setVisibility(8);
        this.bPF.setVisibility(8);
        this.bPz.setVisibility(8);
    } else if (adUIStrategy == 2) {
        this.bPA.setVisibility(8);
        this.bPy.setVisibility(8);
        this.bPz.setVisibility(8);
        this.bSv.setVisibility(8);
        this.bSq.setVisibility(8);
        this.bSq.setOnTouchListener(null);
    } else if (adUIStrategy == 3) {
        this.bPA.setVisibility(8);
        this.bPF.setVisibility(8);
        boolean isMute = isMute();      // 检查广告是否处于静音状态
        this.bPL = isMute;
        setAdMute(isMute, false);
    } else {
        this.bPF.setVisibility(0);
        TextView textView = this.bPA;
        if (!this.mIsLand) {
            i2 = 0;
        }
        textView.setVisibility(i2);
        boolean isMute2 = isMute();
        this.bPL = isMute2;
        setAdMute(isMute2, false);
        Xk();
    }
    if (this.mDeliverType != 6) {
        this.bPB.setVisibility(0);     // 设置时间视图可显
    }
    this.bPB.setText(String.valueOf(this.mAdInvoker.getAdDuration()));     // 给时间视图赋值
}

第三处位于 Xc() 函数中,根据 hook 到的函数调用栈,分析其运行过程:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public void Xc() {
    // 获取广告播放时长
    int adDuration = this.mAdInvoker.getAdDuration();   
    String str = adDuration + "";
    ...
 
    jv(adDuration);    // 判断能不能跳过广告
    if (XE()) {
        XH();
    }
    TextView textView = this.bPB;    // 设置剩余时间
    if (textView != null) {
        textView.setText(str);    // 显示非VIP持续时间
    }
    int i = this.mDeliverType;
    if (i == 3 || i == 7) {    // 如果交付类型是37 (VIP广告),广告持续时间小于1,调用dz(false)
        if (adDuration < 1) {
            dz(false);    
        } else {
            this.bSA.setText(str);     // 显示VIP持续时间
        }
    }
    if (this.mDeliverType == 2) {     // 允许跳过的广告
        int Xp = Xp();    // 广告可跳过的剩余时间
        if (Xp < 1) {    // 允许跳过
            Xl();    // 显示跳过按钮
        } else {
            this.bSG.setText(this.mContext.getString(R.string.trueview_accountime, Integer.valueOf(Xp)));
        }
    }
    // 省流:根据不同的交付类型,为不同类型的广告进行时间配置与视图是否可显操作
    ...
}
 
// 处理广告的交互时间限制逻辑
private void jv(int i) {
    // 判断是否为触摸广告,是否支持点击跳转,并且是否已经被点击过
    if (!this.bOR.isTouchAd() || this.bOR.getClickThroughType() != 0 || this.bTn) {
        return;   // 是,直接返回
    }
    // 获取广告的预览信息
    PreAD creativeObject = this.bOR.getCreativeObject();
    // getInterTouchTime()是广告中点击交互的时间间隔,返回 10,表示用户需要等待至少 10 秒之后才能进行一次点击交互。小于0,说明可以点击。
    // 后面一个条件是指当前时间加上最早允许交互的时间点,如果超过广告总时长,则不允许交互,比如总时长120秒,getInterTouchTime() 返回 40,当前时间为100秒,大于总时长,不允许交互。
    if (creativeObject.getInterTouchTime() <= -1 || i + creativeObject.getInterTouchTime() > this.bTp) {
        return;
    }
    // 重置广告界面,继续播放
    this.bSq.reset();
    Wu();
}
 
// 判断当前广告是创意广告
private boolean XE() {
    CupidAD<PreAD> cupidAD = this.bOR;
    if (cupidAD == null || cupidAD.getCreativeObject() == null) {
        return false;
    }
    return this.bOR.getDeliverType() == 10 || this.bOR.getDeliverType() == 11;
}
 
// 计算广告可跳过的剩余时间
private int Xp() {
    if (this.bOR.getDeliverType() != 2) {
        return 0;
    }
    return (this.bOR.getSkippableTime() / 1000) - ((this.bOR.getDuration() / 1000) - this.mAdInvoker.getAdDuration());
}

上面两个函数都是对布局文件进行操作,设置其 text 或者是否可显,并没有判断去掉广告的地方,我们还有继续寻找。

 

对比两个函数发现,获取持续时间的函数是 getAdDuration(),我们去寻找该函数声明,发现在 com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy 类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public int getAdDuration() {
    com.iqiyi.video.qyplayersdk.core.com1 com1Var = this.mPlayerCore;
    if (com1Var == null) {
        return 0;
    }
    return com1Var.getAdsTimeLength();
}
 
// 位于 com.iqiyi.video.qyplayersdk.core.QYBigCorePlayer 类中
public int getAdsTimeLength() {
    com8 com8Var = this.pumaPlayer;
    if (com8Var != null) {
        return Math.round(com8Var.GetADCountDown() / 1000.0f);   // 转成整数
    }
    return 0;
}
 
// com.mcto.player.nativemediaplayer.NativeMediaPlayer 类中
public int GetADCountDown() {
    int GetADCountDown;
    if (IsCalledInPlayerThread()) {      // 判断是否在播放器线程中调用
        return this.native_media_player_bridge.GetADCountDown();    // 获取广告持续时间
    }
    synchronized (this) {
        if (!this.native_player_valid) {     // 判断播放器是否合法
            throw new MctoPlayerInvalidException(puma_state_error_msg);
        }
        GetADCountDown = this.native_media_player_bridge.GetADCountDown();
    }
    return GetADCountDown;
}
 
// com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 类中
public int GetADCountDown() {
        // 调用了一个指定ID43的方法,该方法返回一个JSON格式的字符串,其中包含有关广告信息的数据
    String InvokeMethod = InvokeMethod(43, "{}");
    if (InvokeMethod.isEmpty()) {  // 返回的字符串为空,则表示当前没有广告,方法返回0
        return 0;
    }
    try {
                // 返回的字符串不为空,则将其转换为JSONObject对象,并获取其中名为ad_count_down的值
        return new JSONObject(InvokeMethod).getInt("ad_count_down");
    } catch (JSONException unused) {
        return 0;
    }
}

跟进到 com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 我们就可以发现,该软件是在Native层利用 mediaplay 获取视频时间信息。到这里获取剩余时间的 Java 层分析就差不多可以了。我们可以看到的是在 NativeMediaPlayerBridge 这个类中调用了众多 native 方法去获取广告的各种信息供后续操作,但是将所有的方法全修改一遍不太现实,我们需要寻找判断是否显示广告界面的地方。

4.1.3 分析 QYMediaPlayerProxy 代理类

根据 hook 上层类的方法调用发现,QYMediaPlayerProxy 类中存在一些可能是与加载广告界面相关的函数。

 

 

几个重要的函数分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// setVVCollector():设置VVCollector,收集播放器的VV统计信息。
// video view (VV),意思为视频播放次数,根据广告播放次数,统计盈利。
public void setVVCollector(com.iqiyi.video.qyplayersdk.module.a.f.con conVar) {
    com.iqiyi.video.qyplayersdk.module.a.aux auxVar = this.mStatistics;
    if (auxVar != null) {
        auxVar.setVVCollector(conVar);
    }
}
 
// init(): 初始化播放器界面
// 获取了mControlConfig中的一些配置信息,例如编解码类型、是否自动跳过片头片尾、色盲模式等,然后调用prn.aux构造方法创建一个prn对象,并设置这些配置信息,最后通过a()方法将prn对象和mPassportAdapter对象一起传入a方法中,完成播放器的初始化。
public void init() {
    this.mPlayerCore.a(new prn.aux(this.mControlConfig.getCodecType())
            .eH(this.mControlConfig.isAutoSkipTitle())
            .eI(this.mControlConfig.isAutoSkipTrailer())
            .kR(this.mControlConfig.getColorBlindnessType())
            .lX(this.mControlConfig.getExtendInfo())
            .lY(this.mControlConfig.getExtraDecoderInfo())
            .aie(), com.iqiyi.video.qyplayersdk.core.data.aux.a(this.mPassportAdapter));
}
 
// 检查 RC 策略是否需要执行
// RC 策略是指在不同的地理位置或网络环境下,根据不同的版权限制或合作协议,播放不同的内容或提供不同的服务。
public PlayData checkRcIfRcStrategyNeeded(PlayData playData) {
    if (playData == null) {
        com.iqiyi.video.qyplayersdk.g.aux.d(TAG, "QYMediaPlayerProxy checkRcIfRcStrategyNeeded source == null!");
        return playData;
    }
    int rCCheckPolicy = playData.getRCCheckPolicy();
    com.iqiyi.video.qyplayersdk.g.aux.d(TAG, "QYMediaPlayerProxy checkRcIfRcStrategyNeeded strategy == " + rCCheckPolicy);
    if (this.mPlayerRecordAdapter == null) {
        this.mPlayerRecordAdapter = new PlayerRecordAdapter();
    }
        // 根据 RCCheckPolicy (即 RC 策略) 的值。
        // 如果值为 2,直接返回 playData;如果值为 1 0,,则调用 PlayerRecordAdapter 的 retrievePlayerRecord 方法,获取播放记录,
    return rCCheckPolicy == 2 ? playData : (rCCheckPolicy == 1 || rCCheckPolicy == 0) ?
        com.iqiyi.video.qyplayersdk.player.data.b.con.a(playData, this.mPlayerRecordAdapter.retrievePlayerRecord(playData)) : playData;
}
 
// 获取登录用户信息
void login() {
    IPassportAdapter iPassportAdapter;
        // mPlayerCore 是播放器核心,mPassportAdapter 是用户身份验证适配器。
    if (this.mPlayerCore == null || (iPassportAdapter = this.mPassportAdapter) == null) {
        return;
    }
        // 判断是不是VIP用户,并获取相应用户信息
    this.mPlayerCore.login(com.iqiyi.video.qyplayersdk.core.data.aux.a(iPassportAdapter));
}
 
// 准备播放器重要核心配置
private void prepareBigCorePlayback(PlayData playData) {
    boolean z;
    org.qiyi.android.coreplayer.d.com7.beginSection("QYMediaPlayerProxy.prepareBigCorePlayback");
 
        // 检查是否需要预加载
        com.iqiyi.video.qyplayersdk.h.con conVar = this.mPreload;
    if (conVar != null) {
        conVar.aoj();
    }
 
        // 根据播放数据和控制配置,选择一个播放策略,根据策略选择对应操作
    int a2 = com.iqiyi.video.qyplayersdk.player.data.b.nul.a(playData, this.mContext, this.mControlConfig);
    com.iqiyi.video.qyplayersdk.g.aux.e("PLAY_SDK", "vplay strategy : " + a2);
    switch (a2) {
        case 1:
            performBigCorePlayback(playData);
            break;
        case 2:
            z = true;
            doVPlayBeforePlay(playData, z);
            break;
        case 3:
            doVPlayFullBeforePlay(playData);
            break;
        case 4:
            doVPlayAfterPlay(playData);
            break;
        case 5:
            if (com.iqiyi.video.qyplayersdk.g.aux.isDebug()) {
                throw new RuntimeException("address & tvid & ctype are null");
            }
            com.iqiyi.video.qyplayersdk.g.aux.e("PLAY_SDK", "address & tvid & ctype are null");
            break;
        case 6:
            z = false;
            doVPlayBeforePlay(playData, z);
            break;
    }
    org.qiyi.android.coreplayer.d.com7.endSection();
}
 
// 视频播放结束后,继续获取视频的相关信息。
public void doVPlayAfterPlay(final PlayData playData) {
    performBigCorePlayback(playData);
    lpt6 lpt6Var = this.mTaskExecutor;
    if (lpt6Var != null) {
        lpt6Var.q(new Runnable() { // from class: com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy.1
            @Override // java.lang.Runnable
            public void run() {
                QYMediaPlayerProxy.this.requestVplayInfo(playData);
            }
        });
    }
}
 
// 在获取视频源前获取一些与视频相关的信息
private void doVPlayBeforePlay(PlayData playData, boolean z) {
    VPlayParam a2 = com.iqiyi.video.qyplayersdk.player.data.b.con.a(playData, VPlayHelper.CONTENT_TYPE_PLAY_CONDITION, this.mPassportAdapter);
    this.mVPlayHelper.cancel();
        // 请求 VPlay 信息
    this.mVPlayHelper.requestVPlay(this.mContext, a2, new aux(this, playData, this.mSigt, z), this.mBigcoreVplayInterceptor);
    sendVPlayRequestPingback(true, playData, this.mSigt);
    com.iqiyi.video.qyplayersdk.b.com3.b(playData);
    com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, " doVPlayBeforePlay needRequestFull=", Boolean.valueOf(z));
}
 
// 判断是否需要网络拦截
private boolean isNeedNetworkInterceptor(PlayerInfo playerInfo) {
        // 是否需要忽略用户代理的拦截
    if (ignoreNetworkInterceptByUA()) {
        com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "ignoreNetworkInterceptByUA ");
        return false;
    }
 
        // 判断当前是否处于离线状态,并且要播放的视频是在线视频
    boolean gW = org.iqiyi.video.l.aux.gW(this.mContext);
    boolean D = com.iqiyi.video.qyplayersdk.player.data.b.nul.D(playerInfo);
    if (gW && D) {
                // 获取当前的错误码版本号,根据不同的版本号来执行不同的逻辑
        int errorCodeVersion = getErrorCodeVersion();
        com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "isNeedNetworkInterceptor isOffNetWork = ", Boolean.valueOf(gW), " isOnLineVideo = ", Boolean.valueOf(D), " errorCodeVer = " + errorCodeVersion);
 
                if (errorCodeVersion == 1) {
                        // 自定义错误码为900400的播放器错误
            this.mInvokerQYMediaPlayer.onError(PlayerError.createCustomError(900400, "current network is offline, but you want to play online video"));
            return true;    // 进行网络拦截
 
        } else if (errorCodeVersion == 2) { 
                        // 返回错误码和错误信息
            org.iqiyi.video.data.com7 bbQ = org.iqiyi.video.data.com7.bbQ();
            bbQ.xC(String.valueOf(900400));
            bbQ.setDesc("current network is offline, but you want to play online video");
            this.mInvokerQYMediaPlayer.onErrorV2(bbQ);
            return true;
        }
    }
    return false;     // 不需要进行网络拦截
}

我们重点分析 performBigCorePlayback 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// 执行播放器的核心播放功能
private void performBigCorePlayback(PlayData playData, PlayerInfo playerInfo, String str) {
    int i;
 
        // 判断是否有自定义的播放拦截器(mDoPlayInterceptor),如果有且拦截器拦截了播放请求,则不播放视频。
    com.iqiyi.video.qyplayersdk.f.con conVar = this.mDoPlayInterceptor;
    if (conVar != null && conVar.e(playerInfo)) {
        com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "DoPlayInterceptor is intercept!");
        lpt5 lpt5Var = this.mInvokerQYMediaPlayer;
        if (lpt5Var == null) {
            return;
        }
        lpt5Var.amX();  
 
        // 没有播放器信息,什么都不做
    } else if (this.mPlayerInfo == null) {
    }
 
        // 重点
        else {
        org.qiyi.android.coreplayer.d.com7.beginSection("QYMediaPlayerProxy.performBigCorePlayback");
                // 通过判断播放数据(playData)是否为空以及是否存在播放地址,空则i = 0
        if (com.iqiyi.video.qyplayersdk.player.data.b.nul.A(playerInfo) || playData == null) {
            i = 0;
        } else {
                        // 如果有地址,根据该数据生成CupidVvId,并将该ID与广告相关的Ad对象(mAd)绑定。
                        // 所以这里就是去后台获取广告的id
            com.iqiyi.video.qyplayersdk.cupid.data.model.com9 a2 = com.iqiyi.video.qyplayersdk.cupid.util.con.a(playData, playerInfo, false, this.mPlayerRecordAdapter, 0);
            a2.eV(isIgnoreFetchLastTimeSave());
            int generateCupidVvId = CupidAdUtils.generateCupidVvId(a2, playData.getPlayScene());
            com.iqiyi.video.qyplayersdk.cupid.com4 com4Var = this.mAd;
 
            if (com4Var != null) {
                com4Var.la(generateCupidVvId);    // 更新当前的广告ID
            }
            org.qiyi.android.coreplayer.d.aux.boe();
            i = generateCupidVvId;
        }
 
                // a3 存储广告信息
        com.iqiyi.video.qyplayersdk.core.data.model.com1 a3 = com.iqiyi.video.qyplayersdk.core.data.a.aux.a(this.mSigt, i, playData, playerInfo, str, this.mControlConfig);
        com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, " performBigCorePlayback QYPlayerMovie=", a3);
        this.mPlayerInfo = new PlayerInfo.Builder().copyFrom(playerInfo).extraInfo(new PlayerExtraInfo.Builder().copyFrom(playerInfo.getExtraInfo()).sigt(a3.getSigt()).build()).build();
        // 通知播放器信息已更改(在这里是指开始播放广告)
                notifyPlayerInfoChanged();
                // 判断是否断网
        if (!isNeedNetworkInterceptor(playerInfo)) {
            if (playData == null || (TextUtils.isEmpty(playData.getPlayAddress()) && (TextUtils.isEmpty(playData.getTvId()) || "0".equals(playData.getTvId())))) {
                PlayerExceptionTools.report(0, 0.1f, "1", com.iqiyi.video.qyplayersdk.player.data.b.con.i(playData));
            }
            com.iqiyi.video.qyplayersdk.core.com1 com1Var = this.mPlayerCore;
            if (com1Var != null) {
                com1Var.setVideoPath(a3);    // 设置广告url
                this.mPlayerCore.ahF();
            }
        }
        org.qiyi.android.coreplayer.d.com7.endSection();
    }
}
 
// 停止视频
public void amX() {
    d dVar = this.mQYMediaPlayer;
    if (dVar != null) {
        dVar.stopPlayback();
    }
}
 
// 判断是否获取到视频
public static boolean A(PlayerInfo playerInfo) {
    return z(playerInfo) || y(playerInfo);
}
 
// 获取PlayerExtraInfo对象的播放地址和播放地址类型
public static boolean z(PlayerInfo playerInfo) {
    if (playerInfo == null || playerInfo.getExtraInfo() == null) {
        return false;
    }
    PlayerExtraInfo extraInfo = playerInfo.getExtraInfo();
    String playAddress = extraInfo.getPlayAddress();
    int playAddressType = extraInfo.getPlayAddressType();
    if (TextUtils.isEmpty(playAddress)) {
        return false;
    }
    return playAddressType == 9 || playAddressType == 4 || playAddressType == 8;
}
 
// 判断是否有视频和专辑ID
public static boolean y(PlayerInfo playerInfo) {
    String s = s(playerInfo);     // 专辑ID
    String u = u(playerInfo);     // 视频ID
    if ((TextUtils.isEmpty(s) || TextUtils.equals(s, "0")) && !((!TextUtils.isEmpty(u) && !TextUtils.equals(u, "0")) || playerInfo == null || playerInfo.getExtraInfo() == null)) {
        // 获取PlayerExtraInfo对象的播放地址和播放地址类型
                PlayerExtraInfo extraInfo = playerInfo.getExtraInfo();
        return !TextUtils.isEmpty(extraInfo.getPlayAddress()) && extraInfo.getPlayAddressType() == 6;
    }
    return false;
}
 
// 获取专辑ID
public static String s(PlayerInfo playerInfo) {
    String id;
    return (playerInfo == null || playerInfo.getAlbumInfo() == null || (id = playerInfo.getAlbumInfo().getId()) == null) ? "" : id;
}
 
// 获取视频ID
public static String u(PlayerInfo playerInfo) {
    String id;
    return (playerInfo == null || playerInfo.getVideoInfo() == null || (id = playerInfo.getVideoInfo().getId()) == null) ? "" : id;
}
 
// 一个广告控制器方法,用于更新当前的CupidvvId
public void la(int i) {
        // col=0,则说明当前没有活跃的vvId,打印日志信息表示要更新当前的vvId
    if (this.col.getAndIncrement() == 0) {
        com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_MAIN", "{AdsController}", " update current cupid vvId. current doesn't has active vvId.");
    } else {
        com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_MAIN", "{AdsController}", " update current cupid vvId. but current has active vvId.");
        // 将旧的vvId赋值给coh变量
                this.coh = this.coi;
    }
        // 将当前新的ID赋给coi
    this.coi = i;
    lc(i);
    com5.aux auxVar = this.mQYAdPresenter;
    if (auxVar != null) {
        auxVar.lh(i);    // 为暂停播放函数与继续播放函数传递广告ID
    }
}
 
/*
        该方法用于注册广告委托和委托JSON,以展示广告
        通过 qYPlayerADConfig3.checkRegister 方法判断是否需要注册广告
        通过 Cupid.registerObjectAppDelegate 方法注册代理
        广告类型包括:
        中插广告(SlotType.SLOT_TYPE_BRIEF_ROLL)、
        viewpoint广告(SlotType.SLOT_TYPE_VIEWPOINT)、
        页面广告(SlotType.SLOT_TYPE_PAGE)等等
        代码过长就不再此展示,需要请自行查看
*/
private void lc(final int i) {
    com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK_AD_CORE", "{AdsController}", "; registerCupidJsonDelegate vvId:", Integer.valueOf(i), "");
 
        org.qiyi.android.coreplayer.d.aux.wr(com.qiyi.baselib.utils.d.nul.fJ(org.iqiyi.video.mode.com3.enn) ? 2 : 1);
        ...
        QYPlayerADConfig qYPlayerADConfig5 = this.cog;
      if (qYPlayerADConfig5.checkRegister(256, qYPlayerADConfig5.getAddAdPolicy())) {
          QYPlayerADConfig qYPlayerADConfig6 = this.cog;
          if (!qYPlayerADConfig6.checkRegister(256, qYPlayerADConfig6.getRemoveAdPolicy())) {
              Cupid.registerJsonDelegate(i, SlotType.SLOT_TYPE_VIEWPOINT.value(), this.cof);
          }
      }
        ...
}

我们可以发现这个函数就是判断是否显示广告界面的函数,可以猜测只有当是VIP账户时,播放数据(playData)才为空,才会使 i = 0(广告ID为0)。

4.1.4 修改 if 判断

到这里我们就可以尝试进行破解了,将 if 判断修改,使之进入 i=0 的分支中。

 

 

重打包编译签名,运行程序,已去除视频广告:

 

4.2 总结

4.2.1 破解广告技巧

  • 对于破解视频广告或其他广告,都可以通过获取广告的相关控件,分析函数的调用逻辑顺序,定位到关键类,分析类,找到关键函数。
  • 针对复杂客户端,尽量不采用关键字搜索的方式去破解,因为复杂的客户端代码都是有设计思想的,并且大概率做了混淆,无法轻易通过关键字符串进行定位,可以尝试通过资源 ID 进行定位。
  • 提高英文水平,如 player 代表播放器、Ad Duration 代表广告持续时间等,破解的首要任务就是看懂代码,特别是对于混淆过的代码,那些没有混淆过的函数名、变量名就是破解的关键,只有看懂才有机会能猜到关键点。

4.2.2 扩展:proxy代理类

分析代码后发现,广告的生成、调用、配置大部分都是在 QYMediaPlayerProxy 类中完成的,并且播放器的核心功能也有一部分在代理类中调用。

 

对于第三方SDK动态导入视频广告,通常会通过网络请求向广告服务器发送请求以获取广告,流程参考下方 android 广告 SDK 原理流程图,常用方法使通过动态代理,通过动态代理这样的方法有一定的好处:

  1. 可以过滤和控制广告流量,例如阻止一些恶意或不受欢迎的广告,以及提高广告访问速度和可靠性。
  2. 在特殊情况下,广告服务器可能会要求使用特定的代理服务器或 IP 地址进行广告请求。这时候,动态代理就可以被用来实现这些特殊的网络访问要求,确保广告请求能够成功发送和接收。

进一步分析,我们可以想到广告不太会是在软件刚出来时就加上,一定是后续附加上去的功能。后续除了广告之外肯定也会陆续附加其他功能,如何做到这些功能扩展呢?这就可以用 proxy 代理类了,将播放器核心功能(播放视频)融入到代理类中,让其负责对核心功能进行扩展(如在播放视频之前添加广告)。这样既方便后续软件更新,也会使逻辑更加清晰、出错时能快速定位。

 

android 广告 SDK 原理流程图

 

 

[参考链接]:

 

Android反编译实战-去广告_安卓反编译去除广告_sam.li的博客

 

android广告SDK原理详解(附源码) - 爱码网 (likecs.com)


[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

最后于 2023-4-7 12:51 被I鲸落I编辑 ,原因:
收藏
点赞28
打赏
分享
最新回复 (11)
雪    币: 348
活跃值: (213717)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
shinratensei 1 2023-4-7 16:01
2
0
tql
雪    币: 19803
活跃值: (29410)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-4-7 16:16
3
1
感谢分享
雪    币: 27
活跃值: (221)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
昕雪 2023-4-7 20:10
4
0
感谢分享
雪    币: 9007
活跃值: (3459)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chengdrgon 2023-4-8 11:54
5
0
感谢分享
雪    币: 8146
活跃值: (5340)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
mudebug 2023-4-8 12:28
6
0
不知道法务部什么时候看到这个帖子,然后要求删帖
雪    币: 62
活跃值: (572)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-9 15:45
7
0
支持一下 很详细
雪    币: 674
活跃值: (1684)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
daxia200N 6 2023-4-11 15:47
8
0
不错支持下
雪    币: 32313
活跃值: (7105)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
ninebell 2023-4-11 21:37
9
0
图片全挂了
雪    币: 225
活跃值: (146)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_ggnlrzcs 2023-4-12 17:15
10
0
感谢分享,点赞
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
cx_yiran 2023-4-13 10:24
11
0
哈哈哈哈,这帖子势必活不久,但是感谢分享!已经学会了
雪    币: 3007
活跃值: (3552)
能力值: (RANK:215 )
在线值:
发帖
回帖
粉丝
china 5 2023-5-29 20:45
12
0

在乐视TV上测试了下:

1、输入法没法自动调出。

2、启动能显示推荐,页面上的可以看,如果换到别的页面就提示网络错误,重新链接网络的样子,切换到推荐也不行。

3、还碰到最下面一个什么开通会员去广告的绿色背景白色字的条,然后有闪退。


另外,有没有奇异果的?



在雷电模拟器上测试,同样的问题

最后于 2023-5-29 21:03 被china编辑 ,原因: 修改
游客
登录 | 注册 方可回帖
返回