如何实现 AppStore App 的自动下载

作者: Joy___ | 来源:发表于2018-05-27 23:50 被阅读2274次

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

    最终效果见:百度云盘播放地址

    为什么做这个呢?

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

    什么是 ASO

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

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

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

    如何实现 App 的自动下载

    想要的效果:

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

    大概实现步骤:

    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 的自动登录。

    相关文章

      网友评论

      • 天空总是很晴朗:看了你的文章,还是写不出来
      • 天空总是很晴朗:代码能发我学习下吗
      • 小修哥:大佬,如果我登录成功了,怎么验证是appid 授权了,从而实现评论功能或者下载功能?
      • 最好吃的长豆角:小哥哥,视频看不了啦,再来一个连接
        Joy___:链接: https://pan.baidu.com/s/1UVN9lRfbwxGcuqSVt3-DxQ 密码: 94s5
      • 0x11901:期待介绍刷机、修改UUID等内容的文章(●°u°●)​ 」
      • adc785d915f4:找iOS APP上架
      • soulDxl:厉害了大佬:+1:
      • MdWhat:大佬, 百度云盘视频挂掉了..:joy:
        Joy___:@MdWhat 我为啥能看:joy:
      • 流星载梦:视频看不了,提示加载失败。。
        Joy___:链接: https://pan.baidu.com/s/1dPikupP5qub2AuKGgvG26w 密码: 33k7
      • Joy___:头条头条客户端基础架构组长期招人,欢迎私信内推
      • 时间shiwo9:视频看不了,提示暂不支持播放此格式的视频
        Joy___:@时间shiwo9 好,我更新下

      本文标题:如何实现 AppStore App 的自动下载

      本文链接:https://www.haomeiwen.com/subject/xnbvjftx.html