這次的分享是關於如何在 AppStore 實現 App 的自動下載,理想中的目標是隻需要一部手機,不需要人來干預,就可以模擬使用者的真實下載,並在下載完成以後,可以自動更改手機引數,使之變為另外一部蘋果手機,進行周而復始的下載工作。但是呢,本文的內容只包含如何去模擬使用者的操作來完成下載,並不涉及抹機、IP 更換等內容。
最終效果見: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 優化的手段,幫助提升產品的曝光量。
白帽子 ASO 常用的手段就是通過資料分析,來優化關鍵詞、標題等,進而提高 App 的排名和曝光率。而黑帽子的手段則是,通過刷榜程式來實現 App 的大量搜尋、下載、好評這一系列的過程來提升 App 的排名。
常見的刷榜手段主要有兩種,一種是機刷,就是通過觸動精靈或者程式碼注入的方式來實現模擬使用者的真實操作,進而完成搜尋、下載、評論等操作。再一種協議刷,就是破解 AppStore 的登陸、下載相關的網路協議,通過模擬真實的網路請求來實現登陸、下載等行為。據說在刷榜過程中,蘋果會校驗你的 Apple ID、IP 等資訊,所以需要購買大量的 Apple ID 和不斷更換 IP 地址。
如何實現 App 的自動下載
想要的效果:
- 進入 AppStore,切換 tab 到搜尋介面
- 設定搜尋關鍵字、搜尋
- 進入列表頁後,點選 App 進入詳情頁點選下載
- 根據提示完成登陸、下載,並在下載完成以後跳轉到推薦 Tab
- 進入推薦 Tab 後,退出登陸
大概實現步驟:
- 準備越獄手機和 Mac 電腦
- 砸殼 dumpdecrypted,通常 PP助手、iTools 下載的 App 是經過砸殼的,同時 AppStore App 不需要砸殼
- 標頭檔案獲取:AppStore class-dump,系統庫的標頭檔案的獲取:dyld_cache class-dump
- 定位關鍵函式:Reveal、Cycript、lldb
- 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
本文首先介紹了常見的攻擊手段:
- 通過靜態分析和動態分析掌握 App 的內部邏輯,通過程式碼注入實現我們想要的功能,比如自動下載、自動跳轉等功能
- 通過分析 App 的網路請求,破解網路協議,模擬真實的網路請求來達到某種目的,比如批量下載,批量評論等功能。
然後介紹了 ASO 的影響因素都有哪些,以及黑帽子和白帽子都是怎麼進行 ASO 優化的。最後重點寫了如何一步步通過程式碼注入,實現 AppStore App 的自動登入。