Iphone的captive support network的逆向工程分析

June 09, 2017 | 13 Minute Read

**所有内容只做学习研究之用** 首先要读一下苹果官方文档 ------------------------ 要大概了解状态机上面的各个状态和转移流程,NEHotspotHelper应该就是专门为了wifi 热点辅助 captive portal功能的支持的。如果是苹果内置的网页的插件还不能满足要求 的话,只能自己利用这个接口来开发APP了。 1.Hotspot Network Subsystem Programming Guide https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/Hotspot_Network_Subsystem_Guide/Contents/AuthStateMachine.html 2. NEHotspotHelper Class Reference, https://developer.apple.com/documentation/networkextension/nehotspothelper 模块,线程和事件循环 -------------------- iOS里面相关的功能模块是 “PrivateFramework/SystemConfigure/CaptiveNetworkSupport” 但iOS 10 看上去把http请求和加载的部分分离出来放到一个CaptiveAgent程序里面去了。 CatptiveSupportNetwork 和 CaptiveAgent 不同的进程,CaptiveAgent构建一个专门处理 portal页面http请求的sandbox环境。ios 9.3.5 还没有这个沙盒的,看网上好像是因为有 人报告 cookies泄露等安全问题,苹果才把这个http处理分离出来放到一个沙盒里面执行吧。 两个进程之间通过 XPC https://developer.apple.com/documentation/xpc#overview 来交互。 [CatptiveSupportNetwork] <-----------> [助手类Plugin 响应状态命令] [CatptiveSupportNetwork] <-----------> [CaptiveAgent http沙盒] CatptiveSupportNetwork 和 Plugin的之间的调用通过 CNPluginStateListIssueCommand 函数来进行。 这个插件的通知和调度,定时器事件,应该是运行在一个 CFRunLoop事件循环里面的。不同的事件循环直接还可以通过信号量之类的来进行同步 对Apple的开发不是很熟悉,不过大概是这样子吧。 https://developer.apple.com/documentation/corefoundation/cfrunloop-rht ```text start() // iOS的serice的启动函数吧 ->pthread_create( captived_thread()) captived_thread() CaptivePrefsInit(gThreadRunLoop, prefs_changed); //配置界面 mysyslog_init_logger(0); // 初始化log UIAllowedRegisterForCallbacks(gThreadRunLoop, CaptiveHandleUIAllowedChange); WebSheetHandlerInitialize(CaptiveWebsheetDone); // 内置的,弹出来登陆的那个浏览器 CNPluginHandlerInitialize(CaptiveBuiltinPluginProcessCommand); // 处理wifi状态变化 CFRunLoopRun(v7) // 执行事件循环; CFRunLoopSourceSignal(); CFRunLoopWakeUp(sConfigdRunLoop); CNPluginHandlerInitialize // 初始化插件 CommandHandlerClassRegister(3, CNPluginHandlerMethods); v2 = CommandHandlerClassRegister(5, CNScanListFilterHandlerMethods); my_CFRunLoopSourceCreateForBSDNotification(v4, CNPluginHandlerNWIChanged, 0, &v8); G_builtin_plugin = CNPluginStateBuiltinRegister(v1); //内置的插件 NetIFSetNewInterfaceCallBack(CNPluginHandlerInterfaceAttached); PowerChangeRegister(CNPluginHandlerPowerChange); return NetIFSetScanResultsCallBack(CNScanListFilterHandleScanResults); CNPluginHandlerNetworkInformationChanged // 网络有变化的处理函数? CNInfoNetworkActive // 对应状态机图上的Active状态, 应该就是用户选了某个wifi热点 CaptiveBuiltinPluginProcessCommand 这个就是内置插件的处理命令的函数了 ``` 状态机状态转移函数 ------------------ ```text Function name Segment Start Length Locals Arguments R F L S B T = ------------- ------- ----- ------ ------ --------- - - - - - - - _CNInfoGetCacheEntry __text 00011286 0000003E 0000001C 00000000 R . . . B . . _CNInfoFind __text 000112D4 00000030 00000010 00000000 R . . . B . . _CNInfoEvaluating __text 000113F8 0000050C 00000050 00000000 R . . . B . . _CNInfoAuthenticating __text 00011910 0000033A 00000050 00000000 R . . . B . . _CNInfoMaintaining __text 00011C58 0000033A 00000058 00000000 R . . . B . . _CNInfoPresentingUI __text 00011FA0 00000268 00000054 00000000 R . . . B . . _CNInfoLoggingOff __text 00012208 00000202 00000044 00000000 R . . . B . . _CNInfoSetAuthState __text 00012650 0000016A 0000002C 00000000 R . . . B . . _CNInfoFlushAuthCommand __text 000127BA 0000001E 0000000C 00000000 R . . . B . . _CNInfoFailure __text 00012912 000000B4 00000020 00000000 R . . . B . . _CNInfoAuthenticated __text 00012A08 00000346 0000006C 00000000 R . . . B . . _CNInfoInactive __text 00012F70 00000088 00000010 00000000 R . . . B . . _CNInfoAddExcludedDisplayID __text 00013B58 00000038 00000010 00000000 R . . . B . . _CNInfoFlushFilterCommand __text 00014428 0000001E 0000000C 00000000 R . . . B . . _CNInfoNetworkActive __text 00014596 000002B0 00000040 00000000 R . . . B . . _CNInfoGet __text 00014866 00000138 00000020 00000000 R . . . B . . ``` _CNInfo开头的这些函数应该是状态机状态转移处理函数,对应苹果官方文档的状态机的图上的各个状态。 CNPluginHandleResponse 应该是各个插件处理完成之后,再在这个函数里面调用上面的那些CNInfoxxxx状态转移函数 ``` CNInfoNetworkActive { CNInfoGetCacheEntry NetCacheEntryGetIsCaptive 缓存的network导致是不是captive的 这个函数里面里打印下面这个消息 "cache indicates network not captive" "cached captive network" "cache indicates network not captive" "no cache entry"; "%@: SSID '%@' %smaking interface primary (%s)" if (cache提示不是captive网络的话 ) {// cache indicates network not captive NetIFSetRankDefault(v17); // 马上在wifi上面放行网络流量了,这样微信等应用流量应该马上切换过来了吧 } else { NetIFSetRankNever(v17); // 禁用wifi流量,等到后面认证通过把 } PassiveDetectNewNetwork(v19); if (cache提示不是captive网络的话) { // cache indicates network not captive CNInfoEvaluating(v1, 0, &v25); } else { CNInfoMaintaining(v1, 0, (int)&v25); } } CNInfoEvaluating { if (超时回调回来检查没有件超时没有返回,但之前有其他插件返回非confidence high的) { print “Evaluate timeout fired, using %@" CNInfoAuthenticating() // 选择这个插件开始认证 return } if (超时回调回来检查,所有插件超时没有返回) { print “Evaluate timed out, these plugins did not respond:" CNInfoAuthenticated(v4, 0, 0); // 认为认证通过 return } if (需要通知插件evaluate) { if (CNPluginStateListIssueCommand 通知插件开始执行命令成功) { TimerSet(v38, 45); // 设置定时器45秒超时,在超时时回调我们 } else { CNInfoFailure(v4, 0); // 转入失败状态 } return } else if(是插件返回) { CFDictionaryGetValue(v3[1], CFSTR("RedirectHostname")) PassiveDetectSetRedirectURLHostname(); // 获取 插件探测出来的重定向url responseGetResult(v3[1]); print %@: Evaluate result is %s (%d) // 打印 evaluate 结果 if (插件评估返回的是 IsCaptive) { NetIFSetRankNever(*(_DWORD *)(v4 + 24)); // 暂时禁用网络等到后面认证通过 print "%@: network is captive Confidence %s" } else { print %@: network is not captive } if(是不是所有的插件都处理完成了)等等,这个地方就是按照苹果的Evaluate状态的文档写法一致的吧 if(插件返回需要认证){ CNInfoAuthenticating(v4, 0, 0); } else { CNInfoAuthenticated(v4, 0, 0); // 认为认证通过 } return } } CNInfoAuthenticating { CNPluginStateIssueCommand(v5, v7); TimerSet(v8, 45); // 等待各个插件的响应时间也是45秒 if (如果出现意外情况 ) { 确实会把插件从列表排除,然后返回evaluate阶段再次重新开始 CNInfoAddExcludedDisplayID(v4, v18); CNInfoEvaluating(v4, 0, 0); } } CNInfoPresentingUI { 这个确实根据文档描述一致,这个阶段显示UI后就没有超时的定时器了,一直等下去。 CNPluginStateIssueForegroundCommand(v5, v7) 出错的处理跟CNInfoAuthenticating一样,把插件排除,然后重新用剩下的插件中做evaluate } CNInfoAuthenticated { if (是不是CNPluginHandlerPassiveDetected调用进来的) {// session 超时???? AWDUpdateCaptiveSessionBoolean // 感觉是在用objc_msgSend 把认证结果个各种情况通知给UI展现层 AWDUpdateCaptiveSessionEndDuration(19); CNInfoMaintaining(v4, 0, 0); // 转入 maintaining 状态 PassiveDetectSetNotificationCallBack // 注册事件通知源,网络出问题再通知主动探测? com.apple.symptoms.managed_events.captive-network } else { // 网页认证插件返回的认证结果通过进来的 CNInfoSetAuthState // 这个状态变化的设置 NetIFSetRankDefault(*(_DWORD *)(v4 + 24)); //设置默认网络为 wifi网络? AWDUpdateCaptiveSessionStartDuration // NetCacheUpdateEntry // 缓存认证结果吧 if ( !CNPluginStateIsBuiltin(v39) || ! DisableMaintaining && !PassiveDetectSetNotificationCallBack((int)CNPluginHandlerPassiveDetected, v4) ) { TimerSet(v14, 300); // 设置一个300秒的定时器,调用CNPluginHandlerPassiveDetected吧,跟文档的描述一致 } } } CNInfoMaintaining { if (现在下发maintain命令) { CNPluginStateIssueCommand(v5, v24); // 通知插件处理maintain命令 TimerSet(v25, 45); // maintain阶段,各个插件也只有45秒的时间处理完命令吧 } else { // 插件的处理结果或者定时器超时超时? if (要不要重新认证) { CNInfoAuthenticating(v4, 0, 0); } else { print "%@: Maintain result is %s (%d)" CNInfoAuthenticated(v4, 0, v3); } } } CNInfoInactive { 清理一些回调和定时器 PassiveDetectNewNetwork(v3); 不是很理解这个是干嘛的 NetIFSetRankNever(*(_DWORD *)(v1 + 24)); // 禁用网络从wifi出去了 } CNInfoFailure { if (不知道类的子函数在做什么判断){ NetIFTakeWiFiNetworkOffline() } else { CNInfoInactive(v3); } } ``` 内置插件(__BUILTIN__)的命令处理函数 ----------------------------------- 上面那些是状态机的转移函数,下面重点来了, 这个系统自带的buildin插件的处理函数,如果没有其他第3方wifi助手之类的captive portal插件,iPhone默认的弹认证界面就是由这个内置插件来处理的。 ```c int __fastcall CaptiveBuiltinPluginProcessCommand(int a1) { int command; // r4@1 int commandType; // r5@2 int v3; // r6@2 int cmdUniqueID; // r5@5 int cmdInterfaceName; // r6@5 int network; // r0@5 int network_; // r11@5 int ssidStr; // r8@6 int ssid; // r4@6 _DWORD *i; // r4@7 int result; // r0@10 void *captiveState; // r10@13 int v23; // r1@14 int logger; // r6@18 int v25; // r8@18 int ssidStr_; // r6@20 int is_captive_bypass; // r0@22 int v28; // r5@22 int v29; // r11@23 int v30; // r4@23 int responseDict; // r4@25 int confidence; // r2@25 int respResult; // r1@26 int respUniqueID; // r0@26 int v35; // r4@27 int v36; // r5@27 int v37; // r6@27 __CFString *currentStageString; // r2@30 __CFString *probe_ssid; // r0@30 int probe_cmdID; // r1@30 int response; // r0@31 int v42; // r11@32 int v43; // r4@32 int v44; // r6@32 int v45; // r4@35 int v46; // r5@35 int v47; // r8@38 int v48; // r6@38 int ssidStr___; // r6@42 int v50; // r8@42 int v51; // r1@49 int v52; // r11@51 int v53; // r4@51 int *v54; // r0@52 const char *v55; // r3@52 int v56; // r4@53 int v57; // r6@53 int v58; // r4@56 int v59; // r6@56 int v60; // r11@71 int v61; // r4@71 __CFString **next_stage; // r0@79 int v63; // r11@84 int v64; // r4@84 int *v65; // [sp-8h] [bp-38h]@19 signed int v66; // [sp-4h] [bp-34h]@19 int ssid_; // [sp+0h] [bp-30h]@18 int ssidStr__; // [sp+4h] [bp-2Ch]@18 unsigned __int8 v69; // [sp+Bh] [bp-25h]@22 int cmdTypeValue; // [sp+Ch] [bp-24h]@4 int v71; // [sp+10h] [bp-20h]@1 command = a1; v71 = __stack_chk_guard; if ( a1 ) { commandType = CFDictionaryGetValue(a1, CFSTR("Type")); v3 = CFNumberGetTypeID(); if ( commandType && CFGetTypeID(commandType) == v3 && CFNumberGetValue(commandType, 3, &cmdTypeValue) ) { cmdUniqueID = CFDictionaryGetValue(command, CFSTR("UniqueID")); cmdInterfaceName = CFDictionaryGetValue(command, CFSTR("InterfaceName")); network = CFDictionaryGetValue(command, CFSTR("Network")); network_ = network; if ( network ) { ssidStr = CFDictionaryGetValue(network, CFSTR("SSIDString")); ssid = CFDictionaryGetValue(network_, CFSTR("SSID")); } else { ssid = 0; ssidStr = 0; } captiveState = (void *)CaptiveStateGet(cmdInterfaceName); if ( !captiveState ) { captiveState = malloc(0x78u); *((_DWORD *)captiveState + 29) = 0; *((_DWORD *)captiveState + 28) = 0; _R0 = (int)captiveState + 96; __asm { VMOV.I32 Q8, #0 VST1.32 {D16-D17}, [R0] } _R0 = (int)captiveState + 80; __asm { VST1.32 {D16-D17}, [R0] } _R0 = (int)captiveState + 64; __asm { VST1.32 {D16-D17}, [R0] } _R0 = (int)captiveState + 48; __asm { VST1.32 {D16-D17}, [R0] } _R0 = (int)captiveState + 32; __asm { VST1.32 {D16-D17}, [R0] } _R0 = captiveState; __asm { VST1.32 {D16-D17}, [R0]! VST1.32 {D16-D17}, [R0] } *((_DWORD *)captiveState + 6) = CFRetain(cmdInterfaceName); CFStringGetCString(cmdInterfaceName, (char *)captiveState + 8, 16, 1536); v23 = S_stateHeadStorage; *(_DWORD *)captiveState = S_stateHeadStorage; if ( v23 ) *(_DWORD *)(v23 + 4) = captiveState; S_stateHeadStorage = (int)captiveState; *((_DWORD *)captiveState + 1) = &S_stateHeadStorage; } switch ( cmdTypeValue ) { case 2: // Evaluate 命令 ssid_ = cmdUniqueID; ssidStr__ = ssidStr; logger = mysyslog_get_logger(); v25 = _SC_syslog_os_log_mapping(6); if ( os_log_type_enabled(logger, v25) == 1 ) { *(&ssid_ - 4) = 71303682; *(&ssid_ - 3) = *((_DWORD *)captiveState + 6); v65 = &ssid_ - 4; v66 = 14; _os_log_impl((int)&dword_0, logger, v25, aEvaluate_0, &ssid_ - 4, 14, ssid_, ssidStr__); } CaptiveStateCleanup(captiveState, 1); my_FieldSetRetainedCFType((char *)captiveState + 116, 0); my_FieldSetRetainedCFType((char *)captiveState + 36, ssid); ssidStr_ = ssidStr__; my_FieldSetRetainedCFType((char *)captiveState + 32, ssidStr__); if ( !ssid || !ssidStr_ ) { v56 = mysyslog_get_logger(); v57 = _SC_syslog_os_log_mapping(3); if ( os_log_type_enabled(v56, v57) == 1 ) { v65 = (int *)71303426; v66 = *((_DWORD *)captiveState + 6); v65 = (int *)&v65; v66 = 8; _os_log_impl((int)&dword_0, v56, v57, aSsidIsNull, &v65, 8); } CaptiveSetStage(captiveState, kStage_Unknown); respResult = 1; respUniqueID = ssid_; goto RET_1; } is_captive_bypass = CaptiveBypass(network_, ssidStr_, &v69); v28 = ssid_; if ( is_captive_bypass ) { v29 = mysyslog_get_logger(); v30 = _SC_syslog_os_log_mapping(5); if ( os_log_type_enabled(v29, v30) == 1 ) { *(&ssid_ - 5) = 71303938; *(&ssid_ - 4) = *((_DWORD *)captiveState + 6); *((_BYTE *)&ssid_ - 12) = 64; *((_BYTE *)&ssid_ - 11) = 4; *(int *)((char *)&ssid_ - 10) = ssidStr_; v65 = &ssid_ - 5; v66 = 20; _os_log_impl((int)&dword_0, v29, v30, aNotProbing, &ssid_ - 5, 20); v28 = ssid_; } responseDict = ResponseDictCreate(v28, 0); confidence = 1; RET_SET_CAPTIVE_TRUE: ResponseDictSetIsCaptiveAndConfidence(responseDict, 1, confidence); goto RET_3; } if ( network_ && !my_CFDictionaryGetBooleanValue(network_, CFSTR("WasCaptive"), 0) && my_CFDictionaryGetBooleanValue(network_, CFSTR("IsCached"), 0) ) { v63 = mysyslog_get_logger(); v64 = _SC_syslog_os_log_mapping(5); if ( os_log_type_enabled(v63, v64) == 1 ) { *(&ssid_ - 4) = 71303682; *(&ssid_ - 3) = *((_DWORD *)captiveState + 6); v65 = &ssid_ - 4; v66 = 14; _os_log_impl((int)&dword_0, v63, v64, aNotProbingCach, &ssid_ - 4, 14, ssid_, ssidStr__); v28 = ssid_; } AWDUpdateCaptiveSessionUnsignedInt(0, 0); responseDict = ResponseDictCreate(v28, 0); ResponseDictSetIsCaptiveAndConfidence(responseDict, 0, 0); next_stage = &kStage_Unknown; RET_SET_STAGE: CaptiveSetStage(captiveState, *next_stage); goto RET_3; } if ( CNAccountsIsCarrierSSID(ssidStr_) == 1 ) { v60 = mysyslog_get_logger(); v61 = _SC_syslog_os_log_mapping(5); if ( os_log_type_enabled(v60, v61) == 1 ) { *(&ssid_ - 4) = 71303682; *(&ssid_ - 3) = *((_DWORD *)captiveState + 6); v65 = &ssid_ - 4; v66 = 14; _os_log_impl((int)&dword_0, v60, v61, aFoundCarrierNe, &ssid_ - 4, 14, ssid_, ssidStr__); v28 = ssid_; } *((_BYTE *)captiveState + 114) = 1; responseDict = ResponseDictCreate(v28, 0); confidence = GetConfidenceFromAccounts(captiveState); goto RET_SET_CAPTIVE_TRUE; } currentStageString = kStage_Probe[0]; probe_ssid = (__CFString *)captiveState; probe_cmdID = v28; goto START_PROBE; case 3: // Authenticate 命令 v35 = cmdUniqueID; v36 = mysyslog_get_logger(); v37 = _SC_syslog_os_log_mapping(6); if ( os_log_type_enabled(v36, v37) == 1 ) { *(&ssid_ - 5) = 71303938; *(&ssid_ - 4) = *((_DWORD *)captiveState + 6); *((_BYTE *)&ssid_ - 12) = 64; *((_BYTE *)&ssid_ - 11) = 4; *(int *)((char *)&ssid_ - 10) = v35; HIWORD(v65) = 1088; v66 = *((_DWORD *)captiveState + 10); v65 = &ssid_ - 5; v66 = 20; _os_log_impl((int)&dword_0, v36, v37, aAuthenticate_0, &ssid_ - 5, 20); } if ( *((_BYTE *)captiveState + 114) ) { currentStageString = kStage_Probe[0]; probe_ssid = (__CFString *)captiveState; probe_cmdID = v35; goto START_PROBE; } my_FieldSetRetainedCFType((char *)captiveState + 116, 0); response = CaptiveCopyAuthenticationResult((int)captiveState, network_, v35); goto RET_2; case 4: // PresidentUI 命令 v42 = cmdUniqueID; v43 = mysyslog_get_logger(); v44 = _SC_syslog_os_log_mapping(6); if ( os_log_type_enabled(v43, v44) == 1 ) { *(&ssid_ - 4) = 71303682; *(&ssid_ - 3) = *((_DWORD *)captiveState + 6); v65 = &ssid_ - 4; v66 = 14; _os_log_impl((int)&dword_0, v43, v44, aPresentui_0, &ssid_ - 4, 14); } my_FieldSetRetainedCFType((char *)captiveState + 116, 0); if ( (__CFString *)*((_DWORD *)captiveState + 10) == kStage_UIRequired[0] ) { my_FieldSetRetainedCFType((char *)captiveState + 116, cmdUniqueID); CaptiveInteract(captiveState); break; } v45 = mysyslog_get_logger(); v46 = _SC_syslog_os_log_mapping(3); if ( os_log_type_enabled(v45, v46) == 1 ) { v65 = &v66; v66 = 2; _os_log_impl((int)&dword_0, v45, v46, aPresentuiComma, &v66, 2); } respResult = 0; respUniqueID = v42; goto RET_1; case 5: // Maintain命令 ssid_ = ssid; ssidStr__ = ssidStr; v47 = mysyslog_get_logger(); v48 = _SC_syslog_os_log_mapping(6); if ( os_log_type_enabled(v47, v48) == 1 ) { *(&ssid_ - 4) = 71303682; *(&ssid_ - 3) = *((_DWORD *)captiveState + 6); v65 = &ssid_ - 4; v66 = 14; _os_log_impl((int)&dword_0, v47, v48, aMaintain_0, &ssid_ - 4, 14); } if ( (__CFString *)*((_DWORD *)captiveState + 10) != kStage_Online[0] || network_ && my_CFDictionaryGetBooleanValue(network_, CFSTR("WasJustJoined"), 0) ) { CaptiveStateCleanup(captiveState, 0); } *((_BYTE *)captiveState + 114) = 0; my_FieldSetRetainedCFType((char *)captiveState + 116, 0); my_FieldSetRetainedCFType((char *)captiveState + 36, ssid_); ssidStr___ = ssidStr__; my_FieldSetRetainedCFType((char *)captiveState + 32, ssidStr__); v50 = cmdUniqueID; if ( *((_DWORD *)captiveState + 11) ) captive_agent_abort_probe((_DWORD *)captiveState + 11); if ( *((_DWORD *)captiveState + 12) ) captive_agent_abort_login((char *)captiveState + 48); if ( ssid_ && ssidStr___ ) { if ( CaptiveBypass(network_, ssidStr___, &v69) ) { v51 = 16; ssid_ = v69; if ( v69 ) v51 = 17; AWDUpdateCaptiveSessionUnsignedInt(0, v51); v52 = mysyslog_get_logger(); v53 = _SC_syslog_os_log_mapping(5); if ( os_log_type_enabled(v52, v53) != 1 ) goto SET_ONLINE; *(&ssid_ - 5) = 71303938; *(&ssid_ - 4) = *((_DWORD *)captiveState + 6); *((_BYTE *)&ssid_ - 12) = 64; *((_BYTE *)&ssid_ - 11) = 4; *(int *)((char *)&ssid_ - 10) = ssidStr___; v65 = &ssid_ - 5; v66 = 20; v54 = &dword_0; v55 = aNotProbing; goto NOT_PROBE; } if ( *((_BYTE *)captiveState + 113) ) { v52 = mysyslog_get_logger(); v53 = _SC_syslog_os_log_mapping(5); if ( os_log_type_enabled(v52, v53) != 1 ) { SET_ONLINE: responseDict = ResponseDictCreate(v50, 0); next_stage = kStage_Online; goto RET_SET_STAGE; } *(&ssid_ - 4) = 71303682; *(&ssid_ - 3) = *((_DWORD *)captiveState + 6); v65 = &ssid_ - 4; v66 = 14; v54 = &dword_0; v55 = aNotProbingIgno; NOT_PROBE: _os_log_impl((int)v54, v52, v53, v55, v65, v66); v50 = cmdUniqueID; goto SET_ONLINE; } currentStageString = kStage_Maintain[0]; probe_ssid = (__CFString *)captiveState; probe_cmdID = cmdUniqueID; START_PROBE: response = CaptiveStartProbe(probe_ssid, probe_cmdID, (int)currentStageString); } else { v58 = mysyslog_get_logger(); v59 = _SC_syslog_os_log_mapping(3); if ( os_log_type_enabled(v58, v59) == 1 ) { v65 = (int *)71303426; v66 = *((_DWORD *)captiveState + 6); v65 = (int *)&v65; v66 = 8; _os_log_impl((int)&dword_0, v58, v59, aSsidIsNull, &v65, 8); v50 = cmdUniqueID; } CaptiveSetStage(captiveState, kStage_Unknown); respResult = 1; respUniqueID = v50; RET_1: response = ResponseDictCreate(respUniqueID, respResult); } RET_2: responseDict = response; RET_3: if ( responseDict ) { CNBuiltinPluginProvideResponse(responseDict); CFRelease(responseDict); } break; default: respResult = 3; respUniqueID = cmdUniqueID; goto RET_1; } } } else { for ( i = (_DWORD *)S_stateHeadStorage; i; i = (_DWORD *)*i ) CaptiveStateCancelOperations(i, 0); } result = __stack_chk_guard - v71; if ( __stack_chk_guard != v71 ) __stack_chk_fail(result); return result; } ``` 看内置的插件函数, 在重新Evalute时候会撤销未完成的 websheet登录页面和http probe探测。 进入maintain时会撤销未完成的http probe探测, 通过调用CaptiveStateCleanup或者captive_agent_abort_probe来取消的吧 ```text CaptiveStateCleanup CaptiveStateCancelOperations WebSheetStateIssueExitCommand captive_agent_abort_probe ``` 第三方wifi助手插件的加载和卸载 ```text CNPluginStateListIssueCommandCommon CNPluginStateLaunchApplier CNPluginStateSetProcessAssertion 会打印 Created process assertion for com.tencent.xin (227)这样信息 CNPluginRemove CNPluginStateSetNotRunning 会打印 Removing assertion for com.tencent.xin 这样信息 ``` CaptiveStartProbe 里面会通过xpc的IPC把http的请求发送到CaptiveAgent去进行。 CaptiveAgent主要做一些http response status code还有一些重定向的Location的分析 等等,还有就是WISPr自动登录协议的xml的解析。HTTP协议相关的处理都是在 CaptiveAgent进行的。就一个Probe请求来说,CaptiveSupportNetwork需要提供这几个参数 给CaptiveAgent的吧. CaptiveSupportNetwork在create_probe_request里面建立消息 ```c v27 = CFSTR("http://captive.apple.com/hotspot-detect.html"); // 下面这个UserAgent里面的字符串应该是对应iPhone这个模块的版本了, // 应该可以根据这个来区分不同的iPhone的手机的版本。 v35 = (__CFString *)CFStringCreateWithFormat(kCFAllocatorDefault, 0, CFSTR("%s %@"), "CaptiveNetworkSupport-346.50.1"); xpc_dictionary_set_uint64(v41, (int)"MessageType", 0, 0); xpc_dictionary_set_string(v42, "ProbeURL", v39); xpc_dictionary_set_string(v42, "InterfaceName", v38); xpc_dictionary_set_string(v42, "UserAgent", v52); xpc_dictionary_set_uint64(v42, (int)"ProbeTimeout", v51, a5); ``` CaptiveAgent收到消息的处理 ```text message_type = xpc_dictionary_get_uint64(a2, "MessageType"); switch ( message_type ) { case 0: // Probe Message v8 = xpc_dictionary_get_string(v2, "ProbeURL"); v9 = xpc_dictionary_get_string(v2, "InterfaceName"); v10 = xpc_dictionary_get_string(v2, "UserAgent"); v11 = xpc_dictionary_get_uint64(v2, "ProbeTimeout"); ``` 这个ProbeTimeout等待http请求的超时时间值得看一下,找了一下调用的参数赋值, 最初在CaptiveStartProbe函数里面赋值,然后会通过参数传给captive_agent_send_probe create_probe_request函数 ,是const数据段的一个常量 const int _kWISPrProbeDefaultTimeout = 0; 看起来是0,有点奇怪,最后是转换为ios的API函数CFRunLoopTimerCreate的一个参数吧,但实在看不懂ARM汇编到底这个值 是多少。从测试来看60秒超时captiveagent也没有返回错误的,反而是captivenetworksupport那里的45秒的evaluate阶段 的超时先触发了。可能有个平台gdb调试一下或者,越狱后有些hook框架可以打印一下这个captiveagent里面的超时到底是多少。 其他部分,检查Apple的Success页面的代码 ```c bool CheckHttpResponseContent() { int v0; // r0@1 int v1; // r0@1 int v2; // r0@2 const char *v3; // r0@4 bool result; // r0@6 v0 = xmlDocGetRootElement(); v1 = sub_9DFE(v0, "title", 0); result = 0; if ( v1 ) { v2 = *(_DWORD *)(v1 + 12); if ( v2 ) { if ( *(_DWORD *)(v2 + 4) == 3 ) { v3 = *(const char **)(v2 + 40); if ( v3 ) { if ( !strcmp(v3, "Success") ) result = 1; } } } } return result; } ``` 专门显示html页面供用户操作UI,是叫做一个com.apple.WebSheet的应用里面进行的。 CaptiveNetworkSupport模块属于configd进程,然后探测到portal页面的存在的的话, 他是需要这样通知springboard管理器来启动websheet应用的。和websheet应用之间的交互 应该是通过xpc跨进程调用来进行的。 WebSheetStateLaunchApplication里面会设置一个 定时器,如果发出“launch websheet”请求之后,10秒超时时间到了这个websheet进程还没有 被启动的话,CaptiveNetworkSupport模块会回复websheet died的错误, 参考 CaptiveWebsheetDone里面的返回结果的处理。 ```text CaptiveInteract WebSheetStateLaunchApplication SBSLaunchApplicationWithIdentifier(CFSTR("com.apple.WebSheet"), 1); ``` 有了反汇编的源码就可以结合代码逻辑和iPhone的syslog来分析各种问题了 --------------------------------------------------------------- 1. 网络连接激活之后,缓存里面到底是不是captive network的手机的行为 如果是缓存了的是captive的网络,wifi流量不会没马上放行,先等一个maintain阶段的HTTP/1.0探测结果再放行流量。 如果不是captive网络,马上放行wifi流量,同时也马上开始evaluate阶段, evaluate 也会马上发送http/1.0探测。 也就是说不管是不是“cache indicates network not captive”, 一连网络后苹果肯定马上发送http/1.0 探测的。 如果先前是captive的网络,探测之后还是captive网络,如果检查到需要认证就马上开始认证。 如果先前不是captive网络,evaluate探测之后也会检查出是captive网络,需要认证也可 以马上开始认证。区别是evaluate会所有的插件都参与,可能会有别的插件干扰。如果直接 maintain就只有一个插件(一般是内置的插件)参与。不同的地方就是 如果网络激活时有需要重新验证的话, “cache indicates network not captive”预先 切换过来的流量会失败,导致用户在手机可能会看到一些应用比如微信报告的网络错误。 如果缓存的里面的是captive,就不会有这个问题。另外预先放行流量可能会导致比较多 连接出来,这些连接又被重定向captive portal页面的http服务器上面去。 要避免苹果手机的缓存成非captive网络,只能evaluate首次探测即使不需要认证也给它弹一个简单 的提示框了。 后面的maitain的探测应该不影响是不是captive的判断了。