用到的工具
- IDA Pro 7.0:反编译工具
- Reveal:UI分析工具
- frida-ios-dump:砸壳工具
- class-dump:导OC头文件工具
- Cycript:命令行调试工具
- JFTool:封装Cycript脚本
- Theos:Tweak开发工具包
- iOS 番茄小说 Ver 4.2.0:测试目标APP
砸壳
用 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分析,可以看出滑动结束后只会保留一个控制器,正在滑动过程中,会保留两个控制器。我们可以大胆猜测,插入的广告页和小说页的创建销毁应该是在 BDReaderViewController
、BDSlideViewController
类或他们的父类中进行管理的。
我们先分析 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