继续操作前请注册或者登录。

APP去广告技术分析



本文为看雪论坛优秀文章
看雪论坛作者ID:I鲸落I

准备工作
测试环境
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.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.1 分析步骤
3.1.1 分析广告页面

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

3.1.2 分析启动时函数调用栈
可以猜测 showHomePage() 就是展示我们的主页了,我们逐条分析广告发生前的函数:
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.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()函数中,该函数主要设置广告配置及布置广告界面。
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 到的函数调用栈,分析其运行过程:
 

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) { // 如果交付类型是3或7 (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类中:
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() { // 调用了一个指定ID为43的方法,该方法返回一个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类中存在一些可能是与加载广告界面相关的函数。
 

 
几个重要的函数分析:
// 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函数:
// 执行播放器的核心播放功能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;}// 判断是否有视频和专辑IDpublic 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;}// 获取专辑IDpublic static String s(PlayerInfo playerInfo) { String id; return (playerInfo == null || playerInfo.getAlbumInfo() == null || (id = playerInfo.getAlbumInfo().getId()) == null) ? "" : id;}// 获取视频IDpublic static String u(PlayerInfo playerInfo) { String id; return (playerInfo == null || playerInfo.getVideoInfo() == null || (id = playerInfo.getVideoInfo().getId()) == null) ? "" : id;}// 一个广告控制器方法,用于更新当前的CupidvvIdpublic 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的博客
https://blog.csdn.net/samlirongsheng/article/details/111684432
 
android广告SDK原理详解(附源码) - 爱码网 (likecs.com)
https://www.likecs.com/show-204598624.html

看雪ID:I鲸落I
https://bbs.kanxue.com/user-home-939796.htm
*本文由看雪论坛 I鲸落I 原创,转载请注明来自看雪社区




往期推荐






特斯拉最新电驱控制器变化拆解梳理及其三电系统供应链






仅剩1位73岁开发者苦撑!能求解超复杂物理方程式的计算程序,要没人维护了






【交易技术前沿】国产分布式数据库在证券行业的应用价值






7 种提升 Spring Boot 吞吐量神技




球在看
到顶部