歡迎光臨
每天分享高質量文章

iOS逆向之給騰訊影片App新增快進手勢

來自公眾號:知識小集

作者 | cinvoke,目前在某社交App擔任iOS組長,做過社交、直播、智慧家居等,喜歡逆向、音影片,有一隻叫狗蛋的胖藍貓~

起因

起因是女朋友喜歡追綜藝,現在的綜藝節目中間有時候會插播很長的廣告,正常速度看完的話太浪費時間了,直接拖進度條的話又容易拖過內容,只能點選右上角更多,切換到2x倍速播放,等播放完後,再點選更多,切換到1x倍速,操作比較繁瑣,體驗太差。而芒果tv有個很好的功能就是,長按直接切換到2x倍速,放開恢復1x倍速,大大提升了看劇體驗,所以本文的主題就是,將這功能透過逆向註入,新增到騰訊影片App裡。

其實逆向開發在iOS領域內已經有很完善的工具鏈了,本文主要是提供下分析思路。

脫殼

逆向App的前提是需要已脫殼的ipa包,我們從app store下的ipa包都是經過蘋果加密的,可以從一些第三方市場比如PP助手之類的下載越獄版ipa,這種一般都是脫殼的,還有就是可以透過越獄手機執行App然後把記憶體dump出來重新打成ipa包。

這裡我們用第二種方式,dump的方式也有好幾種,比如越獄應用CrackerXI,可以直接把App dump到手機目錄裡,不過需要手動傳輸ipa包到電腦上。

早期的dump工具基本是用dumpdecrypted[1],不過作者已經不維護了,這裡推薦用Monkey大神的frida-ios-dump[2],在越獄手機上裝對應的frida外掛,然後執行dump.py就可以一步匯出脫殼的ipa包到電腦上:

安裝流程上面地址已經註明,這裡簡單給大家列下:

  1. 在越獄手機上用Cydia商店搜尋安裝frida

  2. clone下上面的專案,使用命令安裝相關python依賴庫 
    sudo pip install -r requirements.txt –upgrade

  3. 安裝usbmuxd,用來把SSH轉發到USB裝置的手機上,安裝後在終端輸入iproxy 2222 22

  4. 終端執行python ./dump.py 對應App的bundleID 開始dump
    ShellCopy

騰訊影片的BundleID對應com.tencent.live4iphone,現在我們開始dump:

python /dump.py com.tencent.live4iphone

完成後就會在當前目錄下生成一個已脫殼的ipa包了。

分析

下麵分步驟分析怎麼定位到快進的相關函式:

1、Debug View

新建MonkeyApp工程

MonkeyApp是一個可以直接執行脫殼ipa的專案,非常方便用於逆向除錯,詳情可以檢視Github[3]

除了用MonkeyApp外,還可以透過註入Reveal.dylib到App內然後用Reveal來檢視,這裡就暫時不擴充套件了。

專案名隨便起個,我這裡用TVHook,然後把我們上面脫殼的ipa包放入TargeApp目錄中。

修改下BundelID和開發者賬號後我們開始執行,第一次執行一般會失敗,這時候我們可以把Targe.plist裡的資訊複製到Info.plist裡,重新改下BundelID後我們繼續執行

修改完畢後可以看到執行成功了,這裡需要註意的是,上面這ipa包是透過哪個架構脫殼的,就只能在哪個架構的機子上執行,比如我的是在arm64上脫殼,就不能再模擬器上(x86_64)和armv7上執行。

我們直接去到影片播放頁面,然後開啟Xcode的Debug View功能:

接下來我們就可以直接看到檢視結構了:

這裡騰訊影片的開發小哥已經幫我們註釋好這些View的作用了,省了不少力氣~

2、定位手勢View

QNBPlayerGestureView就是我們要的手勢層,我們需要在這一層上面新增Long手勢,但在這之前,我們可以先檢視它有哪些方法,說不定裡面已經有了Long手勢了:

一開始我是用class-dump來獲取QNBPlayerGestureView的.h檔案來檢視,但是class-dump後居然沒發現這個檔案,估計是打包在某個Framework裡了,但是一個個Framework查又比較麻煩,於是我直接使用了LLDB命令:

image lookup -rn [QNBPlayerGestureView

這條命令就是查詢QNBPlayerGestureView的所有方法,非常方便

這裡我們可以看到QNBPlayerGestureView確實是打包在QNBAutomatic.framework裡,而且我們還看到了-[QNBPlayerGestureView didLongPress:],這樣的話我們就不需要自己新增long手勢了,後面直接Method swizzle該方法就行。

3、定位2x速入口

手勢的入口我們已經定位到了,接下來就是定位2x快進的方法了,還是和開始一樣,在有2x播放的介面我們使用Debug View,來定位到相關View:

可以看到相關2x按鈕所在是檢視是QNBPlayerRateSelectPanel,繼續用image lookup檢視相關方法:

image lookup -rn [QNBPlayerRateSelectPanel

這裡我們看到了關鍵入口[QNBPlayerRateSelectPanel btnPress:]。
結合[QNBPlayerRateSelectPanel btnArray]和[QNBPlayerRateSelectPanel rateArray]方法我們可以猜測大概邏輯就是,點選觸發btnPress然後透過btnArray和rateArray的對應關係獲取到相應的rate,也就是我們只要知道rateArray的內容就可以知道對應哪個Button了。

下麵我們將列印[QNBPlayerRateSelectPanel rateArray]的值,先打印出QNBPlayerRateSelectPanel的地址:

這裡獲取到的是0x14541b400,接下來列印rateArray

可以看到2倍數就再最後一個,想就是相應的Button就是[btnArray lastObject],也就是我們只要呼叫btnPress:[btnArray lastObject],就能觸發2x倍數的邏輯了。

到此基本也就分析完畢,我們只需要在-[QNBPlayerGestureView didLongPress:]裡觸發QNBPlayerRateSelectPanel的btnPress:[btnArray lastObject]就能實現長按切換2x速播放了。下麵我們開始實現功能。

開發功能

MonkeyApp內部已經實現了註入功能,省去了我們自己註入的步驟,所以我們可以直接在它註入的dylib裡編寫我們的程式碼:

上面的NSObject+CPSwizzle是我自己簡單封裝的Method Swizzle方法,因為後續程式碼編寫都需要用,大概程式碼如下:

+ (void)mothodSwizzleClass:(NSString *)className
                       old:(NSString *)oldMothod
                       new:(NSString *)newMothod
                   isClass:(BOOL)isClass {

    if (isClass) {
        Method oriSessionMethod = class_getClassMethod(NSClassFromString(className), NSSelectorFromString(oldMothod));
        Method mySessionMethod = class_getClassMethod(NSClassFromString(className), NSSelectorFromString(newMothod));
        method_exchangeImplementations(oriSessionMethod, mySessionMethod);
    } else {
        Method oldMothods = class_getInstanceMethod(NSClassFromString(className), NSSelectorFromString(oldMothod));
        Method newMothods = class_getInstanceMethod(NSClassFromString(className), NSSelectorFromString(newMothod));
        BOOL didAddMethod =
        class_addMethod(NSClassFromString(className),
                        NSSelectorFromString(oldMothod),
                        method_getImplementation(newMothods),
                        method_getTypeEncoding(newMothods));
        if (didAddMethod) {
            class_replaceMethod(NSClassFromString(className),
                                NSSelectorFromString(newMothod),
                                method_getImplementation(oldMothods),
                                method_getTypeEncoding(oldMothods));
        } else {
            method_exchangeImplementations(oldMothods, newMothods);
        }
    }
};

當然我們也可以用MonkeyApp自己的Hook功能,不過還是Method Swizzle用的順手點,程式碼入口在CHConstructor裡,我們先實現Loog手勢的Hook:

[NSObject mothodSwizzleClass:@"QNBPlayerGestureView" old:@"didLongPress:" new:@"cp_didLongPress:" isClass:NO];

然後新增對應的方法:

@implementation NSObject (Hook)

- (void)cp_didLongPress:(UILongPressGestureRecognizer *)avg1 {
    [self cp_didLongPress:avg1];
    //這裡開始編寫我們的程式碼
}

接下來我們需要獲取到QNBPlayerRateSelectPanel的物件地址做方法呼叫,這裡有兩種方法,

第一種是透過Hook QNBPlayerRateSelectPanel的init方法,然後透過一個全域性變數儲存init後的物件,然後直接使用這個全域性變數就行,但這有個弊端,就是QNBPlayerRateSelectPanel的retainCount會+1,導致不能正常釋放,想正常釋放還得Hook他父檢視的dealloc方法,然後手動釋放,計較麻煩。

第二種就是透過我們之前獲取到的View結構,遍歷查詢到對應QNBPlayerRateSelectPanelclass

獲取到QNBPlayerRateSelectPanel後我們就可以編寫具體實現程式碼了,具體程式碼如下:

- (void)cp_didLongPress:(UILongPressGestureRecognizer *)avg1 {
    [self cp_didLongPress:avg1];

    //遍歷獲取到QNBPlayerRateSelectPanel
    UIView *superView = [(UIView *)self superview];
    id panle = [superView findQNBPlayerRateSelectPanel];

    //獲取到[QNBPlayerRateSelectPanel btnArray]
    NSArray *btnArray = [panle valueForKey:@"btnArray"];
    for (UIButton *btn in btnArray) {
        btn.selected = NO;
    }
    if (avg1.state == UIGestureRecognizerStateBegan) {
        //手勢開始,取到2x的Button
        UIButton *button = btnArray.lastObject;
        button.selected = YES;
        [panle performSelector:@selector(btnPress:) withObject:button];
    } else if (avg1.state == UIGestureRecognizerStateEnded) {
        //手勢結束,取1x的Button
        UIButton *button = btnArray[2];//透過之前的分析可以知道1x是陣列的第2個下標
        button.selected = YES;
        [panle performSelector:@selector(btnPress:) withObject:button];
    }
}

大概邏輯已經做了註釋,到此已經實現了想要的功能,直接執行看效果!由於效果動圖有點大,無法上傳,所以可以點選“閱讀原文”,檢視實際效果!

結語

雖然到此已經實現了想要的功能,但是每次都會有個Toast提示,很影響觀看,於是想透過函式呼叫邏輯查詢到最終的實現函式,最終發現函式呼叫鏈如下:

直接呼叫[QNBPlayerInfo setRate:@2]就能實現效果,具體的查詢過程等後續有時間在給大家補上。

最後感謝AloneMonkey大神開發了相關的逆向工具,簡化了逆向的複雜度,也推薦大家去瞭解他寫的逆向書籍~

參考

[1]https://github.com/stefanesser/dumpdecrypted
[2]
https://github.com/AloneMonkey/frida-ios-dump
[3]
https://github.com/AloneMonkey/MonkeyDev

已同步到看一看
贊(0)

分享創造快樂