此次的分享是關於如何在 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 的全稱是 App Search Optimization,就是提高你 APP 在 AppStore 排行榜和搜索結果排名的過程。咱們常常能夠看到 AppStore 有一些奇怪的五星好評,也會遇到搜索關鍵字,排名第一的是一個看上去徹底不相關的 App。這些都是 ASO 優化的手段,幫助提高產品的曝光量。bash
白帽子 ASO 經常使用的手段就是經過數據分析,來優化關鍵詞、標題等,進而提升 App 的排名和曝光率。而黑帽子的手段則是,經過刷榜程序來實現 App 的大量搜索、下載、好評這一系列的過程來提高 App 的排名。網絡
常見的刷榜手段主要有兩種,一種是機刷,就是經過觸動精靈或者代碼注入的方式來實現模擬用戶的真實操做,進而完成搜索、下載、評論等操做。再一種協議刷,就是破解 AppStore 的登錄、下載相關的網絡協議,經過模擬真實的網絡請求來實現登錄、下載等行爲。聽說在刷榜過程當中,蘋果會校驗你的 Apple ID、IP 等信息,因此須要購買大量的 Apple ID 和不斷更換 IP 地址。app
想要的效果:框架
大概實現步驟:ide
咱們的 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 是一款 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 是由 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 提升效率。
上面咱們使用 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 將處理往上拋而已。
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 相似)。
那咱們來使用 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 來實現想要的效果便可。其中還有一個較難的點,就是彈窗提示咱們登錄怎麼辦?如何實現自動登陸功能?
首先,想到的就是在 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 {
}
複製代碼
適當增長對 App 安全的精力的投入,像如今業界的不少 App 都處於被破解的狀態,網上隨處可見各類 App 的破解版,好比愛奇藝會員破解、釘釘遠程打卡等。從客戶端角度出發,須要增長代碼混淆、反調試等手段保證運行環境的安全,同時與後端人員合做增長保證網絡數據鏈路、反做弊的手段。
本文首先介紹了常見的攻擊手段:
而後介紹了 ASO 的影響因素都有哪些,以及黑帽子和白帽子都是怎麼進行 ASO 優化的。最後重點寫了如何一步步經過代碼注入,實現 AppStore App 的自動登陸。