iOS逆向番茄小说广告

/ 3

用到的工具

砸壳

frida-ios-dump 砸壳:

➜  ~ dump.py -l | grep 番茄小说
    -  番茄小说                 com.dragon.read
➜  ~ cd Downloads
➜  Downloads dump.py com.dragon.read
Start the target app com.dragon.read
Dumping 番茄小说 to /var/folders/jr/b0jqqd4x75s51yhj_yqhtnkr0000gn/T
start dump /private/var/containers/Bundle/Application/109EB9BB-9830-43DF-B0FB-814A73C5F2E0/Reading.app/Reading
Reading.fid: 100%|████████████████████████████████████████████████████████████████| 68.4M/68.4M [00:01<00:00, 36.0MB/s]
videoOnPlay.json: 75.8MB [00:03, 24.1MB/s]
0.00B [00:00, ?B/s]
Generating "番茄小说.ipa"

导出头文件

解压ipa压缩包,然后用 class-dump 导出 Mack-O 文件中的头文件:

➜  Downloads mkdir dragon
➜  Downloads cd dragon
➜  dragon unzip 番茄小说.ipa
➜  dragon cp Payload/Reading.app/Reading ./
➜  dragon class-dump -H Reading -o Headers

反编译

Mach-O 文件 Reading 拖到IDA中开始反编译,过程要很久,所以先做其他事情。

初步分析

我们发现这个APP是有VIP功能的,所以我们优先从VIP功能入手。在导出的头文件中搜索 vip 字眼,发现有非常多的结果,先凭经验开始查找:

找到了 SSAccountInfo 类,看起来应该是用户信息的模型类,里面有个应该是标识用户是否是VIP用户的属性 isVip,所以我们可以尝试hook这个属性的 getter 方法。

@property(nonatomic) _Bool isVip;

编写hook代码

使用theos的 nic.pl 命令创建一个 Tweak 工程:

➜  dragon nic.pl
NIC 2.0 - New Instance Creator
------------------------------
  [1.] iphone/activator_event
  [2.] iphone/activator_listener
  [3.] iphone/application_modern
  [4.] iphone/application_swift
  [5.] iphone/cydget
  [6.] iphone/flipswitch_switch
  [7.] iphone/framework
  [8.] iphone/library
  [9.] iphone/notification_center_widget
  [10.] iphone/notification_center_widget-7up
  [11.] iphone/preference_bundle_modern
  [12.] iphone/theme
  [13.] iphone/tool
  [14.] iphone/tool_swift
  [15.] iphone/tweak
  [16.] iphone/tweak_with_simple_preferences
  [17.] iphone/xpc_service
Choose a Template (required): 15
Project Name (required): DragonAd
Package Name [com.yourcompany.dragonad]: com.baidu.dragonad
Author/Maintainer Name [feng]:
[iphone/tweak] MobileSubstrate Bundle filter [com.apple.springboard]: com.dragon.read
[iphone/tweak] List of applications to terminate upon installation (space-separated, '-' for none) [SpringBoard]:
Instantiating iphone/tweak in dragonad/...
Done.

编写hook代码,然后测试发现并没有任何卵用,广告没去掉,我的页面的VIP用户标识也无任何改变。

%hook SSAccountInfo
- (_Bool)isVip {
    return YES;
}
%end

继续分析

继续查找头文件,我发现了 SSVipInfo 类,这个看起来更接近请求的响应数据:

@property(copy, nonatomic) NSString *leftTime;
@property(copy, nonatomic) NSString *expireTime;
@property(copy, nonatomic) NSString *isVip;

IDA已经分析完我们的 Mach-O 文件了,我们在IDA中先搜索 SSVipInfo,然后找到这3个属性的 setter 方法的地址:

我准备给这3个方法打断点看看数据是什么格式的,虽然大概猜测也能猜到是时间戳和0/1标识BOOL值,但还是需要稍微验证下。

通过SSH登录手机,debugserver 附加到番茄小说的进程 Reading,开启LLDB调试端口监听:

iPhone:~ root# debugserver localhost:2223 -a Reading
debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-1200.2.12
 for arm64.
Attaching to process Reading...
Listening to port 2223 for a connection from localhost...
Waiting for debugger instructions for process 0.

在Mac端开启LLDB调试器模式:

➜  dragon lldb
(lldb) process connect connect://localhost:2223
Process 13562 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00000001b2f4f8c4 libsystem_kernel.dylib`mach_msg_trap + 8
libsystem_kernel.dylib`mach_msg_trap:
->  0x1b2f4f8c4 <+8>: ret

libsystem_kernel.dylib`mach_msg_overwrite_trap:
    0x1b2f4f8c8 <+0>: mov    x16, #-0x20
    0x1b2f4f8cc <+4>: svc    #0x80
    0x1b2f4f8d0 <+8>: ret
Target 0: (Reading) stopped.

查看APP当前的 ASLR Offset,并给3个 setter 方法打上断点:

(lldb) image list -o -f | grep Reading
[  0] 0x00000000000ac000 /private/var/containers/Bundle/Application/109EB9BB-9830-43DF-B0FB-814A73C5F2E0/Reading.app/Reading(0x00000001000ac000)

(lldb) breakpoint set -a 0x00000000000ac000+0x101613498
Breakpoint 1: where = Reading`___lldb_unnamed_symbol135304$$Reading, address = 0x00000001016bf498

(lldb) breakpoint set -a 0x00000000000ac000+0x1016134AC
Breakpoint 2: where = Reading`___lldb_unnamed_symbol135306$$Reading, address = 0x00000001016bf4ac

(lldb) breakpoint set -a 0x00000000000ac000+0x1016134C0
Breakpoint 3: where = Reading`___lldb_unnamed_symbol135308$$Reading, address = 0x00000001016bf4c0

在APP里进入我的页面并触发断点,然后打印参数发现全是0,所以验证了我上面的猜测:

(lldb) c
(lldb) x/s $x1
0x1d1f9003c: "setExpireTime:"
(lldb) po $x2
0

(lldb) c
(lldb) x/s $x1
0x102c32e0c: "setLeftTime:"
(lldb) po $x2
0

(lldb) c
(lldb) x/s $x1
0x102c52b93: "setIsVip:"
(lldb) po $x2
0

(lldb) c
Process 14000 resuming

再次尝试hook

从命名上判断 expireTime 应该是固定的,而 leftTime 是根据当前时间计算的。

编写hook代码:

%hook SSVipInfo
- (id)leftTime {
    return [NSString stringWithFormat:@"%.0lf", 2534308005 - [[NSDate date] timeIntervalSince1970]];
}

- (id)expireTime {
    return @"2534308005";
}

- (id)isVip {
    return @"1";
}
%end

然后运行测试,个人中心和开通VIP页面已经显示VIP用户了,但是小说阅读页面里的广告还在。说明小说阅读页面,不是直接从 SSVipInfo 类中获取的数据来判断的。

UI分析

Reveal调试

APP进入小说阅读页面,并翻到有广告的页面,打开Reveal,分析广告视图和小说内容视图的结构。

插入的广告页是 SSReadingAdChapterMiddleContentViewController

滑动页面,进入下一页。

小说内容页是 BDRTextContentViewController

再次滑动一些页面,翻到有广告的页面。然后稍微轻轻滑动页面,可以同时看到广告页面和下一页的小说内容。

Cycript调试

然后使用 cycript 调试,拿到当前控制器的层级结构。我这里使用的 JFTool 是基于 mjcript 修改的,可以在这里下载

iPhone:~ root# cycript -p Reading
cy# @import JFTool
{}

cy# JFChildVcs(JFRootVc())

只有广告页的结构:

只有小说页的结构:

广告页和小说页同时存在的结构:

我们发现小说阅读页面插入的广告页有两种控制器类型,我们先不用去管这两种类型的区别,直接想办法把他们从添加的地方移除。

通过上面UI分析,可以看出滑动结束后只会保留一个控制器,正在滑动过程中,会保留两个控制器。我们可以大胆猜测,插入的广告页和小说页的创建销毁应该是在 BDReaderViewControllerBDSlideViewController 类或他们的父类中进行管理的。

我们先分析 BDReaderViewController 类,查看头文件发现有个方法很像是创建目标控制器的(调用方法传入一些参数并返回一个控制器,根据经验判断这种方法一般都是创建这个控制器或者配置这个控制器的方法),也就是上一页或下一页的控制器:

- (id)getTargetVC:(id)arg1 toPageContext:(id)arg2 pageActionType:(unsigned long long)arg3 controlPageType:(unsigned long long)arg4;

我们在IDA中搜索这个方法,直接F5查看伪代码(注释是我后面添加的):

id __cdecl -[BDReaderViewController getTargetVC:toPageContext:pageActionType:controlPageType:](BDReaderViewController *self, SEL a2, id a3, id a4, unsigned __int64 a5, unsigned __int64 a6)
{
  unsigned __int64 v6; // x23
  unsigned __int64 v7; // x24
  id v8; // x20
  BDReaderViewController *v9; // x22
  void *v10; // x27
  __int64 v11; // x1
  void *v12; // x20
  BDReaderPageChangeInfo *v13; // x21
  void *v14; // x0
  void *v15; // x0
  void *v16; // x26
  void *v17; // x23
  void *v18; // x27
  void *v19; // x0
  void *v20; // x28
  void *v21; // x19
  void *v22; // x24
  _BOOL8 v23; // x2
  void *v24; // x0
  void *v25; // x19
  void *v26; // x26
  void *v27; // x0
  void *v28; // x24
  _BOOL8 v29; // x2
  struct objc_object *v30; // x0
  __int64 v31; // x0
  __int64 v32; // x24
  __int64 v33; // x19
  void *v34; // x0
  __int64 v35; // x22
  __int64 v36; // x1
  __int64 v37; // x23
  struct objc_object *v38; // x0
  __int64 v39; // x0
  __int64 v40; // x19
  void *v41; // x0
  __int64 v42; // x22
  BDReaderModel *v43; // x0
  void *v44; // x0
  void *v45; // x24
  void *v46; // x0
  void *v47; // x25
  BDReaderModel *v48; // x0
  void *v49; // x0
  void *v50; // x19
  void *v51; // x0
  void *v52; // x28
  BDReaderModel *v53; // x0
  __int64 v54; // x22
  __int64 v55; // x19

  v6 = a6;
  v7 = a5;
  v8 = a4;
  v9 = self;
  v10 = (void *)objc_retain(a3, a2);
  v12 = (void *)objc_retain(v8, v11);
  // 创建BDReaderPageChangeInfo对象,根据名字猜测应该是页面切换信息类
  v13 = objc_msgSend(&OBJC_CLASS___BDReaderPageChangeInfo, "new");
  -[BDReaderPageChangeInfo setPageActionType:](v13, "setPageActionType:", v7);
  v14 = -[BDReaderViewController pageChangeFrom](v9, "pageChangeFrom");
  -[BDReaderPageChangeInfo setChangeFrom:](v13, "setChangeFrom:", v14);
  // 这里是控制页面切换方向的,比如向前还是向后翻页
  if ( v6 == 2 )
  {
    v15 = objc_msgSend(v10, "chapterInfo");
    v16 = (void *)objc_retainAutoreleasedReturnValue(v15);
    v17 = v10;
    v18 = objc_msgSend(v16, "index");
    v19 = objc_msgSend(v12, "chapterInfo");
    v20 = (void *)objc_retainAutoreleasedReturnValue(v19);
    v21 = objc_msgSend(v20, "index");
    objc_release(v20);
    objc_release(v16);
    if ( v18 == v21 )
    {
      v22 = objc_msgSend(v17, "pageIndex");
      v23 = v22 <= objc_msgSend(v12, "pageIndex");
      -[BDReaderPageChangeInfo setIsForward:](v13, "setIsForward:", v23);
    }
    else
    {
      v24 = objc_msgSend(v17, "chapterInfo");
      v25 = (void *)objc_retainAutoreleasedReturnValue(v24);
      v26 = objc_msgSend(v25, "index");
      v27 = objc_msgSend(v12, "chapterInfo");
      v28 = (void *)objc_retainAutoreleasedReturnValue(v27);
      v29 = v26 <= objc_msgSend(v28, "index");
      -[BDReaderPageChangeInfo setIsForward:](v13, "setIsForward:", v29);
      objc_release(v28);
      objc_release(v25);
    }
    v10 = v17;
    v6 = 2LL;
  }
  else
  {
    -[BDReaderPageChangeInfo setIsForward:](v13, "setIsForward:", v6 == 0);
  }
  -[BDReaderViewController setPageChangeInfo:](v9, "setPageChangeInfo:", v13);
  // 根据命名猜测,这是就是尝试获取插入广告页的控制器。并且会有创建失败的判断,所以我们可以从这里入手,让插入广告页的控制器创建永远都返回nil
  v30 = -[BDReaderViewController tryGetInsertedVC:fromPageContext:toPageContext:](
          v9,
          "tryGetInsertedVC:fromPageContext:toPageContext:",
          v13,
          v10,
          v12);
  v31 = objc_retainAutoreleasedReturnValue(v30);
  v32 = v31;
  if ( v31 )
  {
    // 如果获取到了插入广告页的控制器,就直接返回
    v33 = objc_autoreleasePoolPush(v31);
    v34 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("insert vc:%@, type:%d"), v32, v6);
    v35 = objc_retainAutoreleasedReturnValue(v34);
    sub_100C94E48(
      2LL,
      "BDReader",
      "-[BDReaderViewController(Util) getTargetVC:toPageContext:pageActionType:controlPageType:]",
      v35);
    objc_release(v35);
    objc_autoreleasePoolPop(v33);
    v37 = v32;
  }
  else
  {
    // 没有获取到取插入广告页的控制器,创建正常阅读文字页面的控制器
    v38 = -[BDReaderViewController getVCWithPageContext:pageChangeInfo:](
            v9,
            "getVCWithPageContext:pageChangeInfo:",
            v12,
            v13);
    v37 = objc_retainAutoreleasedReturnValue(v38);
    v39 = objc_release(v32);
    if ( v37 )
    {
      // 如果创建正常阅读文字页面的控制器成功,直接返回
      v40 = objc_autoreleasePoolPush(v39);
      v41 = objc_msgSend(
              &OBJC_CLASS___NSString,
              "stringWithFormat:",
              CFSTR("normal change, from page:%@, to page:%@"),
              v10,
              v12);
      v42 = objc_retainAutoreleasedReturnValue(v41);
      sub_100C94E48(
        2LL,
        "BDReader",
        "-[BDReaderViewController(Util) getTargetVC:toPageContext:pageActionType:controlPageType:]",
        v42);
      objc_release(v42);
      objc_autoreleasePoolPop(v40);
    }
    else
    {
      // 如果创建正常阅读文字页面的控制器失败,调用readerModel的delegate回调方法。并返回空
      v43 = -[BDReaderViewController readerModel](v9, "readerModel");
      v44 = (void *)objc_retainAutoreleasedReturnValue(v43);
      v45 = v44;
      v46 = objc_msgSend(v44, "delegate");
      v47 = (void *)objc_retainAutoreleasedReturnValue(v46);
      if ( (unsigned int)objc_msgSend(
                           v47,
                           "respondsToSelector:",
                           "onBDReaderDidGetEmptyPageFromPageContext:toPageContext:pageChangeInfo:readerModel:") )
      {
        v48 = -[BDReaderViewController readerModel](v9, "readerModel");
        v49 = (void *)objc_retainAutoreleasedReturnValue(v48);
        v50 = v49;
        v51 = objc_msgSend(v49, "delegate");
        v52 = (void *)objc_retainAutoreleasedReturnValue(v51);
        v53 = -[BDReaderViewController readerModel](v9, "readerModel");
        v54 = objc_retainAutoreleasedReturnValue(v53);
        objc_msgSend(
          v52,
          "onBDReaderDidGetEmptyPageFromPageContext:toPageContext:pageChangeInfo:readerModel:",
          v10,
          v12,
          v13,
          v54);
        objc_release(v54);
        objc_release(v52);
        objc_release(v50);
      }
      objc_release(v47);
      objc_release(v45);
    }
  }
  v55 = objc_retain(v37, v36);
  objc_release(v55);
  objc_release(v13);
  objc_release(v12);
  objc_release(v10);
  return (id)objc_autoreleaseReturnValue(v55);
}

编写hook代码

%hook BDReaderViewController
- (id)tryGetInsertedVC:(id)arg1 fromPageContext:(id)arg2 toPageContext:(id)arg3 {
    return nil;
}
%end

测试发现阅读页面插入的广告页面已经不会出现了,但有些还会出现看视频广告的提示语,我们可以简单粗暴的隐藏这个视图。

隐藏视频广告提示语

%hook SSAdReaderCommonEntranceView
- (id)initWithFrame:(struct CGRect)arg1 {
    id view = %orig;
    [view setAlpha:0];
    [view setHidden:YES];
    return view;
}
%end

再次测试,发现成功移除视频广告提示语,避免勿触后弹出广告。

最终代码

#import <UIKit/UIKit.h>
// 不知道有没有作用,先写着吧
%hook SSAccountInfo
- (_Bool)isVip {
    return YES;
}
%end

%hook SSVipInfo
// VIP剩余时间的时间戳
- (id)leftTime {
    return [NSString stringWithFormat:@"%.0lf", 2534308005 - [[NSDate date] timeIntervalSince1970]];
}

// VIP到期时间的时间戳
- (id)expireTime {
    return @"2534308005";
}

// 是否是VIP
- (id)isVip {
    return @"1";
}
%end

// 穿山甲广告SDK,经测试发现如果初始化失败阅读页面会直接闪退。这条路走不通
%hook BUAdSDKManager
%end

// 移除阅读页面的插入广告页
%hook BDReaderViewController
- (id)tryGetInsertedVC:(id)arg1 fromPageContext:(id)arg2 toPageContext:(id)arg3 {
    return nil;
}
%end

// 移除阅读页面视频广告提示语
%hook SSAdReaderCommonEntranceView
- (id)initWithFrame:(struct CGRect)arg1 {
    id view = %orig;
    [view setAlpha:0];
    [view setHidden:YES];
    return view;
}
%end