今日头条旗下XX小说去广告

说在前面

  • 本文警告:此文仅限于技术交流,如果损害了App方利益,请发邮件zhulongfei28@gmail.com,谢谢。
  • 开发工具:
    • Reveal查看App界面
    • Frida跟踪方法调用过程
    • class-dump导出头文件
    • IDA分析Mach-O文件
    • theos开发tweak插件
    • restore-symbol恢复符号表
  • 本文目标:去除开屏广告、下载广告、页内广告

事前准备

  • 使用frida-tools和frida-ios-dump进行脱壳
  • Mach-O拖入IDA进行分析
  • restore-symbol恢复Mach-O符号表后替换原有Mach-O文件
  • theos新建一个tweak项目

开屏广告

  • App启动时会有5s的开屏广告,广告过后会直接进入App里面。这个时候迅速用Reveal查看,要不然就很快跳过去了。本人试了好几遍才捕捉到这个界面,发现广告是由SSReadingAdSplashCSJViewController控制的。
  • 使用frida-trace追踪控制器的所有方法,找到哪里对这个控制器进行初始化。把初始化的条件破坏,就不会进行初始化了,也就没有所谓的广告了。-f选项加上App的BundleID表示重启App进行跟踪,
    1
    frida-trace -U  -f com.dragon.read -m "*[SSReadingAdSplashCSJViewController *]"
  • 命令执行后,会打印如下内容,这个时候需要打印堆栈,查看哪里调用了init方法
    1
    2
    3
    4
    Started tracing 45 functions. Press Ctrl+C to stop.
    /* TID 0x403 */
    2786 ms -[SSReadingAdSplashCSJViewController init]
    2787 ms -[SSReadingAdSplashCSJViewController setDelegate:0x12e0e7ae0]
  • 进入如下路径
    1
    __handlers__文件夹 -> SSReadingAdSplashCSJViewController文件夹 -> init.js
  • 找到init.js文件,修改以下内容。按control + c终止程序,执行以上跟踪方法,会重新进入App。
    1
    2
    3
    4
    onEnter(log, args, state) {
    log(`-[SSReadingAdSplashCSJViewController init]`);
    log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n\t'));
    },
  • 调用堆栈确实打印出来了,一层一层好多调用。有loadtask关键字的方法表示已经在调用广告了,这个应该不是我们需要的。SSReadingAdSplashService表示跟开屏广告有关的服务,大概率这个跟是否开启广告有着密切的关系,我们需要从这个类入手。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Started tracing 45 functions. Press Ctrl+C to stop.
    /* TID 0x403 */
    2585 ms -[SSReadingAdSplashCSJViewController init]
    2585 ms 0x101420158 Reading!-[SSReadingAdSplashCSJTask setupSplashController]
    0x10141f934 Reading!-[SSReadingAdSplashCSJTask load]
    0x10126b4a0 Reading!-[SSReadingAdSplashLoader flushQueue:]
    0x10126ae70 Reading!-[SSReadingAdSplashLoader onTaskCompletion:error:]
    0x10126ad54 Reading!0x5a2d54
    0x1013fccac Reading!-[SSReadingAdSplashBrandTask onCompletionWithError:]
    0x1013fc7a8 Reading!-[SSReadingAdSplashBrandTask onAdLoadFailureWithError:]
    0x1013fb458 Reading!-[SSReadingAdSplashBrandTask load]
    0x10126b4a0 Reading!-[SSReadingAdSplashLoader flushQueue:]
    0x10126a328 Reading!-[SSReadingAdSplashLoader load]
    0x10157eaf8 Reading!-[SSReadingAdSplashService loadSplashWithHotLaunch:]
    0x10157ee84 Reading!-[SSReadingAdSplashService appLaunchSplash]
    0x1014be6b8 Reading!-[SSReadingAppDelegate appWillFinishLaunchWithOptions:]
  • 跟踪SSReadingAdSplashService的所有方法,仔细查看发现shouldShowSplashAd是否展示开屏广告,终于找到了,需要验证一下猜想
    1
    2
    3
    4
    5
    6
    7
    8
    9
    frida-trace -U  -f com.dragon.read -m "*[SSReadingAdSplashService *]"

    Started tracing 21 functions. Press Ctrl+C to stop.
    /* TID 0x403 */
    2344 ms -[SSReadingAdSplashService onServiceInit]
    2345 ms | -[SSReadingAdSplashService addObservers]
    2345 ms | -[SSReadingAdSplashService shouldShowSplashAd]
    ....
    2349 ms -[SSReadingAdSplashService appLaunchSplash]
  • 找到shouldShowSplashAd.js,更改返回值为0,表示不展示广告,按control + c终止程序,重新进执行以上跟踪方法,发现不会出现开屏广告
    1
    2
    3
    onLeave(log, retval, state) {
    retval.replace(0)
    }
  • 大胆在Tweak.x中写入一下代码,从此告别了开屏广告,美滋滋呀,美滋滋。
    1
    2
    3
    4
    5
    %hook SSReadingAdSplashService
    - (BOOL)shouldShowSplashAd {
    return NO;
    }
    %end

下载广告

  • 随便找一本小说进入,然后后点击下载,会有如下弹框,让我们看完广告去下载。要不然每次点击下载都要看广告,这样也太烦了。
  • 通过Reveal发现弹框属于SSNewKCAlertView视图,跟踪SSNewKCAlertView所有方法。
    1
    frida-trace -U -m "*[SSNewKCAlertView *]" XX小说
  • 当点击下载时,会首先执行如下方法进行初始化弹框操作。打印此方法的调用堆栈,找到哪里进行了广告弹框的初始化操作
    1
    58574 ms  -[SSNewKCAlertView initWithStyle:0x0 title:0x103320958 detail:0x0 actions:0x14e6b2990]
  • 找到initWithStyle_title_detail_actions_.js文件,在入口处添加如下代码,打印函数调用堆栈和模块在内存中的偏移地址
    1
    2
    3
    4
    5
    6
    7
    8
    onEnter(log, args, state) {
    let funcAddr = Module.findExportByName('dyld', '_dyld_get_image_vmaddr_slide')
    let funcBody = new NativeFunction(funcAddr, 'ulong', ['ulong'])
    let aslrAddr = funcBody(0).toString(16)
    log('ASLR偏移地址为:0x' + aslrAddr)
    log(`-[SSNewKCAlertView initWithStyle:${args[2]} title:${args[3]} detail:${args[4]} actions:${args[5]}]`);
    log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n\t'));
    },
  • 中断方法执行,再次执行跟踪方法,重新点击下载,会打印如下内容
    1
    2
    3
    4
    5
    6
    7
    3443 ms  ASLR偏移地址为:0x288000
    3443 ms -[SSNewKCAlertView initWithStyle:0x0 title:0x1024b0958 detail:0x0 actions:0x111b66a80]
    3443 ms 0x100c87ef4 Reading!+[SSCommonMethod showAlertViewWithStyle:title:detail:action:darkMode:showCloseBtn:]
    0x1009ce37c Reading!-[SSReaderManager inspire_showInspireAlertWithBookId:confirm:]
    0x1004d9b3c Reading!-[SSReaderManager downloadBookAfterWatchInspire]
    0x100511d28 Reading!-[SSReaderManager onDownloadBtnClick:]
    0x100935fd0 Reading!-[SSReadingNavigationView onDownloadBtnClick:]
  • 不难发现以下两个方法是已经在执行弹框操作了,不在研究范围之内,直接忽略掉。
    1
    2
    +[SSCommonMethod showAlertViewWithStyle:title:detail:action:darkMode:showCloseBtn:]
    -[SSReaderManager inspire_showInspireAlertWithBookId:confirm:]
  • 下面方法是关注的重点,大概意思就是观看完广告视频才能下载,我们需要知道满足什么条件会执行这个方法,我们让它不满足这个条件,就会直接执行下载操作。
    1
    -[SSReaderManager downloadBookAfterWatchInspire]
  • 计算0x100511d28-0x288000(ASLR偏移地址)=0x100289D28的结果,拿0x100289D28放到IDA进行地址搜索,发现定位到如下位置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void __cdecl -[SSReaderManager onDownloadBtnClick:](SSReaderManager *self, SEL a2, id a3)
    {
    ...
    // 此处为定位(光标)位置
    -[SSReaderManager onReaderDownloadBtnClick](self, "onReaderDownloadBtnClick", a3);
    ...
    objc_msgSend(v8, "report_click_reader:content:bookType:", v10, CFSTR("download"), v14);
    ...
    }
  • 打开onReaderDownloadBtnClick方法实现的伪代码,发现快200行代码,这也太多了,怎么找呢?一行一行看,直到发现downloadBookAfterWatchInspire方法。经过耐心寻找,一直找到最后才发现相关代码。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    v56 = +[SSABTestHelper buzz_book_download_inspire_type](
    &OBJC_CLASS___SSABTestHelper,
    "buzz_book_download_inspire_type");
    if ( v56 == (void *)2 )
    {
    v57 = "downloadBookAfterWatchInspire";
    goto LABEL_12;
    }
    if ( v56 == (void *)1 )
    {
    v57 = "downloadBookDirectly";
    goto LABEL_12;
    }
    if ( !v56 )
    {
    LABEL_17:
    v57 = "downloadBookAfterBuyVip";
    LABEL_12:
    objc_msgSend(v2, v57);
    }
  • 通过上面代码分析,得出如下结论
    • 下载方法有三种:downloadBookAfterWatchInspire表示看完广告视频进行下载,downloadBookAfterBuyVip表示购买完vip就行下载,downloadBookDirectly表示直接进行下载
    • 通过v56的值也就是buzz_book_download_inspire_type的值判断进行哪种方式的下载
    • 设置值为1,也就是+[SSABTestHelper buzz_book_download_inspire_type] = 1时,可以直接进行下载
  • 打开Tweak.x文件,写下如下代码。安装后,重新点击下载,发现可以直接下载,不用看任何广告。
    1
    2
    3
    4
    5
    %hook SSABTestHelper
    + (long long)buzz_book_download_inspire_type {
    return 1;
    }
    %end

页内广告

  • 随便找个小说翻几页就会出现一个广告,这样的阅读体验让人感到难受极了。出现广告的时候用Reveal查看发现属于SSReadingAdChapterMiddleContentViewController,需要追踪这个控制的所有方法:
    1
    frida-trace -U -m "*[SSReadingAdChapterMiddleContentViewController *]" XX小说
  • 发现每出现一个广告,都会调用如下方法。
    1
    187872 ms  -[SSReadingAdChapterMiddleContentViewController initWithModel:0x157ceeb70]
  • 找到initWithModel.js,添加以下内容,打印调用堆栈
    1
    2
    3
    4
    onEnter(log, args, state) {
    log(`-[SSReadingAdChapterMiddleContentViewController initWithModel:${args[2]}]`);
    log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n\t'));
    },
  • 调用堆栈层级比较多,把重要的贴出来,如下。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    frida-trace -U -m "-[SSReadingAdChapterMiddleContentViewController initWithModel:]" XX小说
    ....
    /* TID 0x303 */
    2802 ms -[SSReadingAdChapterMiddleContentViewController initWithModel:0x158c5c7f0]
    2802 ms 0x10173b688 Reading!-[SSReadingAdManager chapterMiddleAdVCWithReaderModel:curPageContext:targetPageContext:pageChangeInfo:adATData:adCSJData:]
    0x1013cbd34 Reading!-[SSReadingAdManager satiAdWithReaderModel:curPageContext:targetPageContext:pageChangeInfo:]
    0x101509338 Reading!-[SSReadingAdManager getBusinessInsertedVCWithReadModel:curPageContext:targetPageContext:pageChangeInfo:]
    0x1010a9e14 Reading!-[SSReaderManager requestInsertedVCWithReadModel:curPageContext:targetPageContext:pageChangeInfo:]
    0x10195ecb4 Reading!-[BDReaderViewController tryGetInsertedVC:fromPageContext:toPageContext:]
    ....
  • 可以清晰地发现SSReadingAdManager是一个跟广告有关的管理器,顾名思义,出现了广告才会出现管理器。广告哪里来,肯定是从网络请求的,而SSReaderManager requestInsertedVCWithReadModel就像是从网路请求广告数据。这是我们的猜测,在Tweak.x中写如下代码,验证一下。
    1
    2
    3
    4
    5
    %hook SSReaderManager
    - (id)requestInsertedVCWithReadModel:(id)arg1 curPageContext:(id)arg2 targetPageContext:(id)arg3 pageChangeInfo:(id)arg4 {
    return nil;
    }
    %end
  • 手机注销后,随便打开一个小说,随便翻看,发现再也没有广告了。