如何实现 AppStore App 的自动下载

此次的分享是关于如何在 AppStore 实现 App 的自动下载,理想中的目标是只须要一部手机,不须要人来干预,就能够模拟用户的真实下载,并在下载完成之后,能够自动更改手机参数,使之变为另一部苹果手机,进行周而复始的下载工做。可是呢,本文的内容只包含如何去模拟用户的操做来完成下载,并不涉及抹机、IP 更换等内容。ios

最终效果见:https://pan.baidu.com/play/video#/video?path=%2F自动下载效果视频.mp4&t=-1后端

为何作这个呢?

可能会有人问,为何要作这么一个项目。主要是两点缘由吧,第一点呢,是出于我的兴趣,逆向其实在开发中的用处仍是蛮大的,好比帮助咱们分析 Apple 操做系统,帮咱们作好安全防护。经过这么一个项目的实践,能够加深本身对逆向开发的理解,第二点呢,就是 App Search Optimization 是一个一直比较热门的话题,有白帽子和黑帽子 ASO 之分,经过关键字和标题优化等手段来进行 ASO 的属于白帽子 ASO,而经过刷榜程序来进行 ASO 的属于黑帽子 ASO,ASO 的刷榜脚本是价值不菲的,可能价值几十万甚至几百万。经过这个项目也是小试牛刀,了解下灰产的一些技术手段。安全

什么是 ASO

ASO 的全称是 App Search Optimization,就是提高你 APP 在 AppStore 排行榜和搜索结果排名的过程。咱们常常能够看到 AppStore 有一些奇怪的五星好评,也会遇到搜索关键字,排名第一的是一个看上去彻底不相关的 App。这些都是 ASO 优化的手段,帮助提高产品的曝光量。bash

白帽子 ASO 经常使用的手段就是经过数据分析,来优化关键词、标题等,进而提升 App 的排名和曝光率。而黑帽子的手段则是,经过刷榜程序来实现 App 的大量搜索、下载、好评这一系列的过程来提高 App 的排名。网络

常见的刷榜手段主要有两种,一种是机刷,就是经过触动精灵或者代码注入的方式来实现模拟用户的真实操做,进而完成搜索、下载、评论等操做。再一种协议刷,就是破解 AppStore 的登录、下载相关的网络协议,经过模拟真实的网络请求来实现登录、下载等行为。听说在刷榜过程当中,苹果会校验你的 Apple ID、IP 等信息,因此须要购买大量的 Apple ID 和不断更换 IP 地址。app

如何实现 App 的自动下载

想要的效果:框架

  1. 进入 AppStore,切换 tab 到搜索界面
  2. 设置搜索关键字、搜索
  3. 进入列表页后,点击 App 进入详情页点击下载
  4. 根据提示完成登录、下载,并在下载完成之后跳转到推荐 Tab
  5. 进入推荐 Tab 后,退出登录

大概实现步骤:ide

  1. 准备越狱手机和 Mac 电脑
  2. 砸壳 dumpdecrypted,一般 PP助手、iTools 下载的 App 是通过砸壳的,同时 AppStore App 不须要砸壳
  3. 头文件获取:AppStore class-dump,系统库的头文件的获取:dyld_cache class-dump
  4. 定位关键函数:Reveal、Cycript、lldb
  5. tweak 的注入

砸壳

咱们的 App 上传到 AppStore 后,苹果会对 App 进行加密,要想去分析可执行文件,就必需要进行脱壳解密的操做,dumpdecrypted 是一款出色的脱壳工具,它的原理是将 App 运行起来,App 启动时,系统会对 Mach-O 文件进行加载,并完成对应的解密操做,dumpdecrypted 就能够在此时将解密后的 Mach-O dump 出来,从而达到解密的效果。函数

若是为了省事能够直接从 PP 助手、iTools 上下载对应的 App,通常状况下是已经通过砸壳的。同时,对于 AppStore 这样的系统程序有些特殊,他们 并不须要进行砸壳,能够直接拿来进行分析。工具

获取头文件

拿到一个砸壳后的可执行文件后,就可使用 class-dump 来获取可执行文件的全部头文件,class-dump 会对 Mach-O 的格式进行分析,并将信息提取出来造成咱们想要的头文件。

AppStore 的可执行文件也略有特殊,class dump以后会发现 AppStore 中包含的代码极少。App Store 的不少关键代码逻辑都不在 AppStore 这个可执行文件当中,而是在系统的动态库中,咱们须要分析动态库的头文件信息进而定位到关键函数。能够获取对应系统dyld_cache 中的动态库,而后 dump 出头文件。AppStore UI 有关的逻辑都在 StoreKitUI 动态库中,这个动态库是分析的重点。

Reveal

Reveal 是一款 UI 调试工具,官方的定义是:See your iOS application's view hierarchy at runtime with advanced 2D and 3D visualisations,固然对于逆向安全人员,查看本身 App 的布局是彻底不够的,咱们能够在 Cydia 中下载 Reveal Loader,在同一网段下,经过 Mac 的 Reveal 和 iOS 上的 Reveal Loader 就能够查看任意 App 的 UI 布局。

可是,有时候咱们不只想要去看这个 UI 布局,还想要去动态调试这个布局,去看它的 Controller 是谁,去挖掘界面下的真正的代码逻辑。这个就涉及到 Cycript 这个工具。

Cycript

Cycript 是由 Cydia 创始人 Saurik 推出的一款脚本语言,它混合了Objective-C 与 JavaScript 两种语法,很容易上手,咱们能够经过 Cycript 来进行动态调试,好比查看函数运行的效果,寻找 View 的 Controller 等。

就拿上面 Reveal 详情页为例, Reveal 能够看到获取按钮是 SKUIOfferView,列表页是一个 SKUICollectionView ,那么就经过 Cycript 来看看控制这个 SKUICollectionView 的 Controller 是谁。首先经过 OpenSSH 来链接 iPhone,经过 cycript -p AppStore 来对 AppStore 进行注入调试,UIApp.keyWindow.recursiveDescription().toString() 来打印视图层级。(注:此截图和后面的地址对不上,由于不是同一次打印,你们了解下大概意思就成)

能够发现 SKUICollectionView,而且它的内存地址是 0x13fa00e00,能够经过 cycript 脚原本找到它的 Controller 是哪个,有多种方案,好比经过它的 delegate 来找,或者经过 nextResponder 来找均可以。

cy# [#0x13fa00e00 delegate]
#"<SKUIStorePageSectionsViewController: 0x140167e00>"

cy# [#0x13fa00e00 nextResponder]
#"<UIView: 0x140f5f540; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x140f771c0>>"
cy# [#0x140f5f540 nextResponder]
#"<SKUIStorePageSectionsViewController: 0x140167e00>"
复制代码

同时也能够借助一些私有 API 来实现快速查找 ViewController,使用[[[UIWindow keyWindow] rootViewController] _printHierarchy].toString(),能够发现打印结果中一样能够找到 SKUIStorePageSectionsViewController

cy# [[[UIWindow keyWindow] rootViewController] _printHierarchy].toString()
`<SKUITabBarController 0x157815400>, state: appeared, view: <UILayoutContainerView 0x156db38e0>
   | <UINavigationController 0x15784d200>, state: disappeared, view: <UILayoutContainerView 0x156e6b240> not in the window
   |    | <SKUIDocumentContainerViewController 0x1578d3c00>, state: disappeared, view: <UIView 0x1580e1aa0> not in the window
   |    |    | <SKUIStackDocumentViewController 0x15812b740>, state: disappeared, view: <UIView 0x1580dc870> not in the window
   |    |    |    | <SKUIStorePageSectionsViewController 0x1578ec000>, state: disappeared, view: <UIView 0x1580f1a30> not in the window
   |    |    |    |    | <SKUIAccountButtonsViewController 0x158654180>, state: disappeared, view: <SKUIAccountButtonsView 0x158654f60> not in the window
   | <UINavigationController 0x157849c00>, state: disappeared, view: <UILayoutContainerView 0x156ec4df0> not in the window
   | <UINavigationController 0x157803600>, state: disappeared, view: <UILayoutContainerView 0x156e80de0> not in the window
   | <UINavigationController 0x15703ea00>, state: appeared, view: <UILayoutContainerView 0x156f114a0>
   |    | <SKUIDocumentContainerViewController 0x157ab2a00>, state: disappeared, view: <UIView 0x158a25930> not in the window
   |    |    | <SKUIStackDocumentViewController 0x158a50690>, state: disappeared, view: <UIView 0x158a2b360> not in the window
   |    |    |    | <SKUIStorePageSectionsViewController 0x1578e6000>, state: disappeared, view: <UIView 0x158a2d4b0> not in the window
   |    | <SKUIDocumentContainerViewController 0x157b5fa00>, state: appeared, view: <UIView 0x158cf70e0>
   |    |    | <SKUIStackDocumentViewController 0x158cf6690>, state: appeared, view: <UIView 0x158cf72b0>
   |    |    |    | <SKUIStorePageSectionsViewController 0x157b4ae00>, state: appeared, view: <UIView 0x158cfb1e0>
   | <UINavigationController 0x157028000>, state: disappeared, view: <UILayoutContainerView 0x156ef1300> not in the window
   |    | <ASUpdatesViewController 0x156f169e0>, state: disappeared, view: <UIView 0x156dbd590> not in the window`

复制代码

从上面的分析能够知道,SKUICollectionView 的控制器是 SKUIStorePageSectionsViewController,「获取」按钮的类是 SKUIOfferView,下一步是分析头文件,看看有没有能够比较明显的方法能够为咱们所用。下载是最关键的一步,那么首先来看看 SKUIOfferView 类的状况,它的头文件大体如此。

#import <StoreKitUI/SKUIItemOfferButtonDelegate-Protocol.h>
#import <StoreKitUI/SKUIViewElementView-Protocol.h>

@class NSMapTable, NSMutableArray, NSString;
@protocol SKUIOfferViewDelegate;

@interface SKUIOfferView : SKUIViewReuseView <SKUIItemOfferButtonDelegate, SKUIViewElementView> {
    unsigned long long _alignment;
    NSMapTable *_buttonElements;
    NSMapTable *_buyButtonDescriptorToButton;
    struct UIEdgeInsets _contentInset;
}
- (void)_buttonAction:(id)arg1;

- (void)itemOfferButtonWillAnimateTransition:(id)arg1;
- (void)itemOfferButtonDidAnimateTransition:(id)arg1;
- (struct CGSize)sizeThatFits:(struct CGSize)arg1;
复制代码

能够从头文件中看到一个 _buttonAction 方法,感受上是 「获取」按钮点击后的响应方法,对于这种猜想,可使用 Cycript 来进行调试,测试一下这个函数执行的效果到底如何 在终端执行 [#0x156c69cc0 _buttonAction:#0x156cb4d20] 后查看效果以下,App 已经开始进行下载了,说明这个方法的效果咱们猜对了,在调试过程当中,能够多多使用 Cycript 提升效率。

lldb

上面咱们使用 Cycript 测试了 _buttonAction 的效果,可是这个方法有一个参数,咱们要搞清楚它正确的参数类型,传入正确的值。这时候能够借助 LLDB ,来帮助咱们找到这个参数的正确类型。 可使用 b function 来针对 _buttonAction 方法打断点,而后打印它的参数。

传统的作法是使用LLDB 和 IDA 等工具找到 ASLR 和 基地址等信息,而后计算出符号的地址,这样作起来比较繁琐,仍是能够继续使用一些私有方法快速定位 _buttonAction 的符号地址来进行断点。

咱们想要断点的方法是 _buttonAction,它所在的类是 SKUIOfferView,那么可使用 LLDB 输入 po [SKUIOfferView _shortMethodDescription] 来看下效果:(更多强大的黑科技私有函数能够参考这里:http://iosre.com/t/powerful-private-methods-for-debugging-in-cycript-lldb/3414)

(lldb) po [SKUIOfferView _shortMethodDescription]
<SKUIOfferView: 0x1a096ddd8>:
in SKUIOfferView:
	Class Methods:
		+ (void) requestLayoutForViewElement:(id)arg1 width:(double)arg2 context:(id)arg3; (0x194719470)
		+ (CGSize) sizeThatFitsWidth:(double)arg1 viewElement:(id)arg2 context:(id)arg3; (0x1947197a8)
	Properties:
		@property (weak, nonatomic) <SKUIOfferViewDelegate>* delegate;  (@synthesize delegate = _delegate;)
		@property (nonatomic) long metadataPosition;  (@synthesize metadataPosition = _metadataPosition;)
		@property (readonly, nonatomic, getter=isShowingConfirmation) BOOL showingConfirmation;  (@synthesize showingConfirmation = _isShowingConfirmation;)
	Instance Methods:
		- (BOOL) setImage:(id)arg1 forArtworkRequest:(id)arg2 context:(id)arg3; (0x19471a8c8)
		- (BOOL) updateWithItemState:(id)arg1 context:(id)arg2 animated:(BOOL)arg3; (0x19471a8d0)
		- (void) _buttonAction:(id)arg1; (0x19471bb5c)
		- (BOOL) _shouldHideNoticesWithBuyButtonDescriptor:(id)arg1 context:(id)arg2; (0x19471c368)
		- (void) _positionNoticeForItemOfferButton:(id)arg1; (0x19471c234)
(SKUIViewReuseView ...)

复制代码

能够看到 - (void) _buttonAction:(id)arg1; (0x19471bb5c),那么直接使用 b 0x19471bb5c为 _buttonAction 加断点便可。断点到之后,再打印它的参数,对于 Objective-C 来讲消息有两个隐含参数,也就是 self 和 _cmd,那么咱们想要的参数就在第三个位置,能够经过 po $x2 来查看它的具体信息(ARM64 下函数的参数是存放在 X0 到 X7 这 8 个寄存器里面的,若是超过8个参数,就会入栈)。

Process 7839 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 2.1 3.1
    frame #0: 0x000000019471bb5c StoreKitUI`-[SKUIOfferView _buttonAction:]
StoreKitUI`-[SKUIOfferView _buttonAction:]:
->  0x19471bb5c <+0>:  stp    x24, x23, [sp, #-0x40]!
    0x19471bb60 <+4>:  stp    x22, x21, [sp, #0x10]
    0x19471bb64 <+8>:  stp    x20, x19, [sp, #0x20]
    0x19471bb68 <+12>: stp    x29, x30, [sp, #0x30]
Target 0: (AppStore) stopped.
(lldb) po $x0
<SKUIOfferView: 0x1596aae00; frame = (279 74; 26 26); layer = <CALayer: 0x1596676b0>>

(lldb) po $x2
<SKUIItemOfferButton: 0x1596ab260; baseClass = UIControl; frame = (0 0; 26 26); clipsToBounds = YES; alpha = 0.2; tintColor = UIDeviceRGBColorSpace 0.0862745 0.0156863 0.0156863 1; animations = { opacity=<CABasicAnimation: 0x1592e7b20>; }; layer = <CALayer: 0x15967d9c0>>
复制代码

由上可知,参数类型是 SKUIItemOfferButton,也就是 SKUIOfferView 的 subView,其实点击的是 SKUIItemOfferButton,只是 SKUIItemOfferButton 将处理往上抛而已。

Tweak 注入

Cydia 创始人 Saurik 同时为咱们提供了一个 Cydia Substrate 这么一个工具,官方的定义是:The powerful code modification platform behind Cydia。咱们能够基于 Cydia Substrate 来开发具备各类功能的代码注入程序。

Cydia Substrate 由 MobileHooker、MobileLoader、Safe mode 三个模块组成。MobileHooker 主要用来替换函数的实现,能够想象成 Runtime 的 Method Swizzle。MobileLoader 是用来加载第三方 dylib 的,咱们写的破解程序会在目标程序启动时注入到目标程序。Safe mode 就是安全模式,咱们写 tweak 的时候可能会形成 Crash,好比万一形成 SpringBoard 无限 Crash 手机岂不是就无法用了,因此提供了这么一个安全模式。

MobileHooker 提供了一些函数来让咱们完成 Hook 的工做,可是咱们不直接使用 它们,咱们使用基于他们封装的 Logos 工具,Logos 的语法很简单直观,易于上手。好比 %hook 能够指定要 Hook 的类、%orig 能够执行被钩住的函数的原始实现、%new 给一个现成的 class 添加新函数(效果与 class_addMethod 相似)。

Tweak AppStore

那咱们来使用 Logos 实现下载的功能,当进入 SKUIStorePageSectionsViewController 页面后,找到下载按钮,而后点击下载,当下载按钮的文字由「获取」变为「打开」,表明下载已完成,而后继续执行后续操做。

%hook SKUIStorePageSectionsViewController
- (void)viewDidAppear:(BOOL)animated {	
    %log;
	%orig;
     
    // 遍历全部子 View,找到 offerButton 、offerView
	[self findAllSubviews:self.view];

	if (offerButton && offerView) {
        // 执行下载操做
	    [offerView _buttonAction:offerButton];
        // 每秒去 check 一下,是否下载完成
	    downloadTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
	}		
}
%new
-(void)timerAction {
	if ([offerButton.title isEqualToString:@"打开"]) {
        // 发送下载完成的通知
		[[NSNotificationCenter defaultCenter] postNotificationName:@"textChangedAction" object:nil];

		downloadTimer = nil;
	}
}
%new
-(void)findAllSubviews:(UIView *)view
 {
    for (UIView *subView in view.subviews) {
        if (subView.subviews.count) {
            [self findAllSubviews:subView];
        }
        
        if ([subView isKindOfClass:NSClassFromString(@"SKUIOfferView")]) {
			offerView = (SKUIOfferView*)subView;
		}
		if ([subView isKindOfClass:NSClassFromString(@"SKUIItemOfferButton")]) {
			offerButton = (SKUIItemOfferButton*)subView;
		}
    }
}
%end
复制代码

其余的操做,与上述其实很相似,好比搜索、跳转都是利用静态或者动态分析找到关键函数,经过 tweak 来实现想要的效果便可。其中还有一个较难的点,就是弹窗提示咱们登录怎么办?如何实现自动登陆功能?

Tweak SpringBoard

首先,想到的就是在 AppStore App 中注入代码,Hook UIAlertAction 和 UIAlertController 的代码,会发现并无产生做用。AppStore 中的弹窗不是它来控制的,而是另一个进程 SpringBoard,因此要想实现 Hook AppStore 的弹窗,必须对 SpringBoard 进行代码注入。

咱们正常若是要实现一个这种弹窗,代码通常是这么写

UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:@"标题" message:@"注释信息" preferredStyle:UIAlertControllerStyleActionSheet];  

UIAlertAction *action1 = [UIAlertAction actionWithTitle:@"标题1" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {  
    NSLog(@"点击了按钮 1");  
}];  
UIAlertAction *action2 = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {  
    NSLog(@"点击了按钮 2");  
}];  
  
[actionSheet addAction:action1];  
[actionSheet addAction:action2];  

[self presentViewController:actionSheet animated:YES completion:nil];  
复制代码

基于上面的代码分析可得,咱们要想实现自动登陆,就要实现自动点击「使用现有的 Apple ID」执行系统的原 action 操做,而后在帐号和密码的 TextField 中填入帐号密码,点击「好」执行系统的原始 action 操做。其实能够发现,要执行的 action 实际上是在初始化 UIAlertAction 过程当中,handler block 中加入的逻辑。那么咱们就能够 Hook actionWithTitle:style:handler: 而后将 handler 保存下来,当填写好帐号密码后,主动触发 handler 便可。

上面那种方法也能够奏效,可是须要本身额外处理下 alertView 的出现和消失, 为了简单能够直接尝试第二种方法,在分析 UIKit 框架中 UIAlertController 类的头文件时发现 _dismissWithAction:这个方法,而后我就试了一下发现能够完成 dismiss 和 执行 handler 两项功能,因此我就直接使用了这个 API 来模拟点击。核心代码以下:

typedef void(^CDUnknownBlockType)(UIAlertAction *action);
CDUnknownBlockType testBlock;
static UIAlertAction *keepAction;
static int atimers;

%hook UIAlertController
- (void)viewDidAppear:(BOOL)animated {
	%log;
	%orig;

	if ([keepAction.title isEqualToString:@"使用现有的 Apple ID"]) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

			((void ( *)(id, SEL, UIAlertAction*))objc_msgSend)(self, NSSelectorFromString(@"_dismissWithAction:"),keepAction);
    	});
    } 

	if ([keepAction.title isEqualToString:@"好"]) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
        	if (self.textFields.count > 1) {
				self.textFields.firstObject.text = @"joyme0104@163.com";
				self.textFields.lastObject.text = @"Joyme0304&&&";

				((void ( *)(id, SEL, UIAlertAction*))objc_msgSend)(self, NSSelectorFromString(@"_dismissWithAction:"),keepAction);

			}
        });
    }
}
%end

%hook UIAlertAction
+ (id)_actionWithTitle:(id)arg1 descriptiveText:(id)arg2 image:(id)arg3 style:(long long)arg4 handler:(CDUnknownBlockType)arg5 shouldDismissHandler:(CDUnknownBlockType)arg6 {
	id obj = %orig;
	UIAlertAction *action = (UIAlertAction *)obj;
    if ([action.title isEqualToString:@"使用现有的 Apple ID"]) {
        testBlock = arg6;
		keepAction = obj;
    } 
	if ([action.title isEqualToString:@"好"]) {
		testBlock = arg6;
		keepAction = obj;
	}
	return obj;
}
%end
复制代码

从代码能够看出咱们在 Hook UIAlertAction 的 _actionWithTitle 方法时,并无 Hook actionWithTitle:style:handler: ,由于我测试的时候发如今我操做过程当中并无触发,怀疑是苹果没有使用这个 API,直接使用了下面这个方法。

+ (id)_actionWithTitle:(id)arg1 descriptiveText:(id)arg2 image:(id)arg3 style:(long long)arg4 handler:(CDUnknownBlockType)arg5 shouldDismissHandler:(CDUnknownBlockType)arg6 {
}
复制代码

Thinking About The Future

适当增长对 App 安全的精力的投入,像如今业界的不少 App 都处于被破解的状态,网上随处可见各类 App 的破解版,好比爱奇艺会员破解、钉钉远程打卡等。从客户端角度出发,须要增长代码混淆、反调试等手段保证运行环境的安全,同时与后端人员合做增长保证网络数据链路、反做弊的手段。

Summary

本文首先介绍了常见的攻击手段:

  1. 经过静态分析和动态分析掌握 App 的内部逻辑,经过代码注入实现咱们想要的功能,好比自动下载、自动跳转等功能
  2. 经过分析 App 的网络请求,破解网络协议,模拟真实的网络请求来达到某种目的,好比批量下载,批量评论等功能。

而后介绍了 ASO 的影响因素都有哪些,以及黑帽子和白帽子都是怎么进行 ASO 优化的。最后重点写了如何一步步经过代码注入,实现 AppStore App 的自动登陆。

相关文章
相关标签/搜索