如有合作,交流方面的意愿,请联系QQ:571652571
iOS抖音App安全机制分析
准备工作
工具
在本文中,用到的工具有IDA Pro, Frida, TheOS,文本编辑器(如VSCode)
脱壳
首先我们进行静态分析,第一步要脱壳,市面上常见的工具有dump-decrypted/frida-ios-dump/clutch等工具。使用frida查看App加载了哪些自带动态库,并对这些模块进行脱壳:
Process.enumerateModulesSync().forEach(function(e){if(e.path.indexOf('.app')!=-1){console.log(e.path)}}) .../Aweme.app/Aweme .../Aweme.app/Frameworks/AgoraRtcEngineKit.framework/AgoraRtcEngineKit .../Frameworks/ByteRtcEngineKit.framework/ByteRtcEngineKit .../Aweme.app/Frameworks/AwemeDylib.framework/AwemeDylib
越狱检测分析
由于抖音二进制相当大,单单IDA分析便需要几十分钟,而越狱逻辑可能隐藏在任何地方,而不限于在进程初始化时(mod_init_func),如果在浩如烟海的代码里去慢慢分析是不可能的,下面就使用最快的思路来解决这个问题
第一阶段 初步分析
首先找到比较明显的痕迹,例如函数名,字符串,http请求body中的关键字(例如jail/break/jeil/jb/cydia/substrate/apt之类的),函数名和字符串可在IDA分析后进行搜索,而http请求可以通过fiddler等抓包工具进行分析:
相关的函数: [ANSMetadata computeIsJailbroken]; [ASSStaticInfoCollector checkJB)]; [ASSStaticInfoCollectorOpen checkJB] [AWECloudJailBreakUtility jailbroken]; [AWEYAMInfoHelper isJailBroken] [BDADeviceHelper isJailBroken]; [BDLogDeviceHelper isJailBroken] [IESLiveDeviceInfo isJailBroken] [HMDCrashBinaryImage isJailBroken]; [HMDInfo isJailBroken] [MobClick isJailbroken] [TTAdSplashDeviceHelper isJailBroken]; [TTInstallDeviceHelper isJailBroken] [UAConveniece deviceWasJailed]; [UIDevice btd_isJailBroken] [UMANProtocolData isDeviceJailBreak] [WXOMTAHelper isJB] 检测逻辑(参考我的另一篇文章"iOS越狱检测研究"): (1)文件系统检测 /bin/bash /Applications/Cydia.app /Library/MobileSubstrate/MobileSubstrate.dylib /user/Applictations /user/Continers/Bundle/Application /usr/sbin/sshd /etc/apt /Applications/RockApp.app, /Applications/Icy.app, /usr/sbin/sshd, /usr/bin/sshd, /usr/libexec/sftp-server, /Applications/WinterBoard.app, /Applications/SBSettings.app, /Applications/MxTube.app, /Applications/IntelliScreen.app, /Library/MobileSubstrate/DynamicLibraries/Veency.plist, /Library/MobileSubstrate/DynamicLibraries/LiveClock.plist, /private/var/lib/apt, /private/var/stash, /private/{uuid}可写 /System/Library/LaunchDaemons/com.ikey.bbot.plist, /System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist, /private/var/tmp/cydia.log, /private/var/lib/cydia, /etc/clutch.conf, /var/cache/clutch.plist, /etc/clutch_cracked.plist, /var/cache/clutch_cracked.plist, /var/lib/clutch/overdrive.dylib, /var/root/Documents/Cracked/ /etc/fstab 分区检测 (2)沙盒完整性检测 system函数 (3)环境变量检测 使用environ检测MobileSubstrate环境变量 (4)mmap检测 (5)进程检测 MobileCydia Cydia afpd (6)schema检测 cydia://
第二阶段 深度分析
上述使用的方式只能找到一些比较肤浅的检测手段,下面来深入挖掘隐藏检测手段。这里我们选择文件系统访问作为切入点,因为越狱检测最基本的方式就是文件系统检测。我们使用frida工具,只需要针对以下系统调用进行跟踪,就可以跟踪出所有可能的函数调用级别的文件操作:access,creat,faccessatgetxattr,getxattr,link,listxattr,lstat,open,opendir,readlink,realpath,stat,statfs,symlink
这里需要注意的一点是,frida工具虽然方便,但是必须在进程启动后才能附加(启动进程的-n参数在老版本iOS有效,10以上失灵),这里用到一个小技法是写一个抖音的tweak,在%ctor中延迟几秒以便我们附加。用此方法也可以跟踪启动时刻的网络请求调用,好处多多。根据附录中的脚本跟踪App得到以下结果:
0x107b49140 AwemeDylib!YjA2MGM5NGQ3MTI5MjljYTA2Y2Y2Yjc3YzM2ZjAxYzAK 0x107b69938 AwemeDylib!0x1f19938 0x107b6e794 AwemeDylib!0x1f1e794 stat /Applications/Cydia.app 0x107b49150 AwemeDylib!YjA2MGM5NGQ3MTI5MjljYTA2Y2Y2Yjc3YzM2ZjAxYzAK 0x107b69938 AwemeDylib!0x1f19938 0x107b6e794 AwemeDylib!0x1f1e794 stat /Library/MobileSubstrate/MobileSubstrate.dylib syscall 5 syscall open /Library/MobileSubstrate/MobileSubstrate.dylib 0x107b49140 AwemeDylib!YjA2MGM5NGQ3MTI5MjljYTA2Y2Y2Yjc3YzM2ZjAxYzAK 0x107b69938 AwemeDylib!0x1f19938 0x107b68c20 AwemeDylib!YzE0NjZkZDJjZWMyYzdhODdkMzNjNjhkYTI2ZmJhNGIK stat /Applications/Cydia.app 0x107b49150 AwemeDylib!YjA2MGM5NGQ3MTI5MjljYTA2Y2Y2Yjc3YzM2ZjAxYzAK 0x107b69938 AwemeDylib!0x1f19938 0x107b68c20 AwemeDylib!YzE0NjZkZDJjZWMyYzdhODdkMzNjNjhkYTI2ZmJhNGIK stat /Library/MobileSubstrate/MobileSubstrate.dylib syscall open /Library/MobileSubstrate/MobileSubstrate.dylib 0x107b4f700 AwemeDylib!MmNiMjczYTdiMjAzZjc5ODljZDg5MDBlZGQ3NmZmOTUK 0x107b68c84 AwemeDylib!YzE0NjZkZDJjZWMyYzdhODdkMzNjNjhkYTI2ZmJhNGIK stat /Applications/iGameGuardian.app 0x107b4f800 AwemeDylib!MmNiMjczYTdiMjAzZjc5ODljZDg5MDBlZGQ3NmZmOTUK 0x107b68c84 AwemeDylib!YzE0NjZkZDJjZWMyYzdhODdkMzNjNjhkYTI2ZmJhNGIK stat /Applications/GamePlayer.app
由上面结果的调用栈,分析AwemeDylib中的代码,可以找到若干个经过函数名混淆的函数,其中两个函数长这样:
__int64 ?????() { v2 = __ldar((unsigned int *)&unk_25B617C); v27 = (unsigned __int64)&unk_25B617C; v21 = "stringWithCString:encoding:"; v20 = "defaultManager"; v19 = "fileExistsAtPath:"; v3 = 3649499418LL; do { while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( (signed int)v3 > 39081286 ) { if ( (signed int)v3 <= 244706462 ) { if ( (signed int)v3 > 126009263 ) { if ( (_DWORD)v3 == 229581047 ) { v0 = v30; v3 = 3270936340LL; } } else if ( (_DWORD)v3 == 39081287 ) { __stlr(0, (unsigned int *)&unk_25B617C); v28 = (Dl_info *)&v18; v6 = __ldar((unsigned int *)&v18); if ( !(unsigned int)&v18 ) { cydialib = 47; // /Applications/Cydia.app byte_2443F01 = 65; .... } __stlr(0, (unsigned int *)&unk_25B6180); v7 = objc_msgSend(&OBJC_CLASS___NSString, v21, &cydialib, 4LL); objc_retainAutoreleasedReturnValue(v7); v8 = objc_msgSend(&OBJC_CLASS___NSFileManager, v20); v9 = v8; v10 = (void *)objc_retainAutoreleasedReturnValue(v8); v11 = (unsigned __int64)objc_msgSend(v10, v19, v7); objc_release(v9); if ( v11 & 1 ) v12 = 1; else v12 = access(&cydialib, 0) == 0; v31 = 538; objc_release(v7); if ( v12 ) v3 = 4208507086LL; else v3 = 2699417432LL; } } else if ( (signed int)v3 <= 1095875126 ) { if ( (_DWORD)v3 == 1034641429 ) v4 = 1; else v4 = v1; if ( (_DWORD)v3 == 1034641429 ) v5 = -257687223; else v5 = v3; if ( (_DWORD)v3 == 244706463 ) v1 = 0; else v1 = v4; if ( (_DWORD)v3 == 244706463 ) v3 = 4037280073LL; else v3 = v5; } else { switch ( (_DWORD)v3 ) { case 0x4151BA37: v0 = 1; v3 = 3270936340LL; break; case 0x5B3EC3D2: v29 = v28; if ( dladdr(&_stat, v28) ) v3 = 3416709257LL; else v3 = 244706463LL; break; case 0x60C3A04F: v23 = 304; v22 = 214; v3 = 1530840018LL; break; } } } if ( (signed int)v3 > -1024030957 ) break; if ( (signed int)v3 > -1595549865 ) { if ( (_DWORD)v3 == -1595549864 ) { v13 = __ldar((unsigned int *)v3); __stlr(0, (unsigned int *)&unk_25B6184); v14 = objc_msgSend(&OBJC_CLASS___NSFileManager, v20); objc_retainAutoreleasedReturnValue(v14); v15 = objc_msgSend(&OBJC_CLASS___NSString, v21, &byte_2443F20, 4LL); objc_retainAutoreleasedReturnValue(v15); v16 = (unsigned __int64)objc_msgSend(v14, v19, v15); objc_release(v15); objc_release(v14); if ( v16 ) v3 = 1095875127LL; else v3 = 1623433295LL; } } else if ( (_DWORD)v3 == -1895695304 ) { libsyskern = 47; // /usr/lib/system/libsystem_kernel.dylib byte_2443ED1 = 117; ..... } else if ( (_DWORD)v3 == -1799416459 ) { if ( !strcmp(v28->dli_fname, &libsyskern) ) v3 = 244706463LL; else v3 = 1034641429LL; } } if ( (signed int)v3 <= -257687224 ) break; if ( (_DWORD)v3 == -257687223 ) { v30 = v1 & 1; v26 = 240; v25 = 208; v3 = 229581047LL; } else { if ( (_DWORD)v3 == -86460210 ) v0 = 1; if ( (_DWORD)v3 == -86460210 ) v3 = 3270936340LL; else v3 = (unsigned int)v3; } } if ( (_DWORD)v3 != -878258039 ) break; v24 = 583; v3 = 2495550837LL; } if ( (_DWORD)v3 != -645467878 ) break; if ( v27 ) v3 = 39081287LL; else v3 = 2399271992LL; } } while ( (_DWORD)v3 != -1024030956 ); return v0 & 1; } void ???(__int64 a1) { v68 = a1; v2 = __ldar(__stack_chk_guard); if ( !__stack_chk_guard ) { v1 = (unsigned int *)&cydia; cydia = 47; // /Applications/Cydia.app byte_24435C1 = 65; .... } __stlr(0, (unsigned int *)&unk_25B5CDC); v3 = __ldar(v1); if ( !(_DWORD)v1 ) { substrate = 47; // /Library/MobileSubstrate/MobileSubstrate.dylib byte_24435E1 = 76; .... } __stlr(0, (unsigned int *)&unk_25B5CE0); v4 = __ldar((unsigned int *)((char *)&dword_0 + 1)); __stlr(0, (unsigned int *)&unk_25B5CE4); v5 = __ldar(v1); if ( !(_DWORD)v1 ) { // /Library/TweakInject twea = 47; byte_2443621 = 76; .... } __stlr(0, (unsigned int *)&unk_25B5CE8); v69 = &v50; memset(&v67, 0, 0x90uLL); v66 = 0; v64 = 0u; v65 = 0u; v63 = 0u; v62 = 0; v60 = 0u; v61 = 0u; v58 = 0; v56 = 0u; v57 = 0u; v55 = 0u; v51 = 0u; v52 = 0u; v54 = 0; v53 = 0u; strcpy((char *)&v63, &cydia); strcpy((char *)&v59, &substrate); strcpy((char *)&v55, &byte_244360F); // file_sys strcpy((char *)&v51, &twea); if ( !__libc_do_syscall(573, v6, v7, v8, v9, v10, v11, v12, &v51, &v67, v37, v38, v39, v40, v41) && (WORD2(v67) & 0xF000) == 40960 || !stat((const char *)&v63, (struct stat *)&v67) ) { goto LABEL_14; } v13 = stat((const char *)&v59, (struct stat *)&v67); if ( !(_DWORD)v13 ) JUMPOUT(__CS__, sub_1EF91A0(v13, v14, v15, v16, v17, v18, v19, v20) + 52); v21 = (void *)syscall(5, &v59, 0LL); if ( (signed int)v21 >= 1 ) { __libc_do_syscall(239, v22, v23, v24, v25, v26, v27, v28, v21, v36, v37, v38, v39, v40, v41); LABEL_14: qword_25B5CB0(6LL, 77LL); v29 = 1LL; goto LABEL_15; } if ( !access((const char *)&v63, 4) ) goto LABEL_14; v29 = 0LL; LABEL_15: v30 = (__int64)v69; v69[1] = 0LL; *(_QWORD *)(v30 + 16) = 0LL; *(_QWORD *)v30 = 0LL; v47 = &off_206CD00; v38 = 0LL; std::__1::ios_base::init((std::__1::ios_base *)&v47, &v40); v48 = 0LL; v49 = -1; v37 = off_206CB88; v47 = (void **)off_206CBD8; v39 = off_206CBB0; std::__1::basic_streambuf<char,std::__1::char_traits<char>>::basic_streambuf(&v40); v42 = 0LL; v40 = off_20C7460; v44 = 0LL; v45 = 0LL; v43 = 0LL; v46 = 24; v31 = strlen((const char *)&v55); v32 = sub_DF61FC(&v39, (__int64)&v55, v31); std::__1::basic_ostream<char,std::__1::char_traits<char>>::operator<<(v32, v29); sub_E21700((__int64 *)&v37, v30); v33 = *(char *)(v30 + 23); if ( v33 >= 0 ) v34 = (void *)v30; else v34 = *(void **)v30; if ( v33 >= 0 ) v35 = *(unsigned __int8 *)(v30 + 23); else v35 = *(_QWORD *)(v30 + 8); std::__1::basic_string<char,std::__1::char_traits<char>,std::__1::allocator<char>>::append(v68, v34, v35); v37 = off_206CB88; v47 = (void **)off_206CBD8; v39 = off_206CBB0; v40 = off_20C7460; if ( SHIBYTE(v44) & 0x80000000 ) operator delete(v42); std::__1::basic_streambuf<char,std::__1::char_traits<char>>::~basic_streambuf(&v40); std::__1::basic_iostream<char,std::__1::char_traits<char>>::~basic_iostream(&v37, off_206CBF8); std::__1::basic_ios<char,std::__1::char_traits<char>>::~basic_ios(&v47); if ( *(char *)(v30 + 23) & 0x80000000 ) operator delete(*(void **)v30); }
从上面结果可以看到,App使用了syscall函数,以及汇编级别的SVC指令来进行越狱检测,其中SVC指令调用的函数都会经过一个导出函数__libc_do_syscall
,我们使用frida跟踪一下该函数:
Interceptor.attach(Module.findExportByName(null, "__libc_do_syscall"), { onEnter: function(args) { var callnum = args[0].toInt32() - 233; if (false) { } else if (callnum == 3) { // read } else if (callnum == 5) { console.log('open ' + args[8].readUtf8String()); } else if (callnum == 6) { // close } else if (callnum == 20) { // getpid } else if (callnum == 39) { // getppid } else if (callnum == 202) { // sysctl } else if (callnum == 294) { // shared_region_check_np } else if (callnum == 340) { console.log('stat64 ' + args[8].readUtf8String()); } else if (callnum == 344) { // getdirentries64 } else { console.log('__libc_do_syscall() ' + callnum); } } });
可得到以下结果:
stat64 /Library/TweakInject stat64 /Library/TweakInject open /Library/LaunchDaemons/ open /Library/MobileSubstrate/DynamicLibraries open /Library/MobileSubstrate/DynamicLibraries/ALS.plist.bak open /Library/MobileSubstrate/DynamicLibraries/AppList.plist open /Library/MobileSubstrate/DynamicLibraries/FLEXing.plist open /Library/MobileSubstrate/DynamicLibraries/GPSTravellerTweakVIP.plist.bak open /Library/MobileSubstrate/DynamicLibraries/MobileSafety.plist open /Library/MobileSubstrate/DynamicLibraries/PreferenceLoader.plist open /Library/MobileSubstrate/DynamicLibraries/RocketBootstrap.plist open /Library/MobileSubstrate/DynamicLibraries/SSLKillSwitch2.plist open /Library/MobileSubstrate/DynamicLibraries/TSActivator.plist open /Library/MobileSubstrate/DynamicLibraries/TSEventTweak.plist
抖音的越狱检测分析到此为止
附录1 系统调用检测
function logtrace(ctx) { var content = Thread.backtrace(ctx.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n'; if (content.indexOf('SubstrateLoader') == -1 && content.indexOf('JavaScriptCore') == -1 && content.indexOf('FLEXing.dylib') == -1 && content.indexOf('NSResolveSymlinksInPathUsingCache') == -1 && content.indexOf('MediaServices') == -1 && content.indexOf('bundleWithPath') == -1 && content.indexOf('CoreMotion') == -1 && content.indexOf('infoDictionary') == -1 && content.indexOf('objectForInfoDictionaryKey') == -1) { console.log(content); return true; } return false; } function iswhite(path) { if (path == null) return true; if (path.startsWith('/var/mobile/Containers')) return true; if (path.startsWith('/var/containers')) return true; if (path.startsWith('/var/mobile/Library')) return true; if (path.startsWith('/var/db')) return true; if (path.startsWith('/private/var/mobile')) return true; if (path.startsWith('/private/var/containers')) return true; if (path.startsWith('/private/var/mobile/Library')) return true; if (path.startsWith('/private/var/db')) return true; if (path.startsWith('/System')) return true; if (path.startsWith('/Library/Preferences')) return true; if (path.startsWith('/Library/Managed')) return true; if (path.startsWith('/usr')) return true; if (path.startsWith('/dev')) return true; if (path == '/AppleInternal') return true; if (path == '/etc/hosts') return true; if (path == '/Library') return true; if (path == '/var') return true; if (path == '/private/var') return true; if (path == '/private') return true; if (path == '/') return true; if (path == '/var/mobile') return true; if (path.indexOf('/containers/Bundle/Application') != -1) return true; return false; } Interceptor.attach(Module.findExportByName(null, "access"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; console.log("access " + path); } }) Interceptor.attach(Module.findExportByName(null, "creat"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("creat " + path); } }) Interceptor.attach(Module.findExportByName(null, "dlopen"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (!iswhite(path)) console.log("dlopen " + path); } }) Interceptor.attach(Module.findExportByName(null, "dlopen_preflight"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (!iswhite(path)) console.log("dlopen_preflight " + path); } }) Interceptor.attach(Module.findExportByName(null, "faccessat"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[1].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("faccessat " + path); } }) Interceptor.attach(Module.findExportByName(null, "getxattr"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("getxattr " + path); } }) Interceptor.attach(Module.findExportByName(null, "link"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("link " + path); } }) Interceptor.attach(Module.findExportByName(null, "listxattr"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("listxattr " + path); } }) Interceptor.attach(Module.findExportByName(null, "lstat"), { block: false, onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("lstat " + path); } }) Interceptor.attach(Module.findExportByName(null, "open"), { onEnter: function(args) { if (args[0].isNull()) return; var path = Memory.readUtf8String(args[0]); if (iswhite(path)) return; if (logtrace(this)) console.log("open " + path); } }) Interceptor.attach(Module.findExportByName(null, "opendir"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("opendir " + path); } }) Interceptor.attach(Module.findExportByName(null, "__opendir2"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("opendir2 " + path); } }) Interceptor.attach(Module.findExportByName(null, "readlink"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("readlink " + path); } }) Interceptor.attach(Module.findExportByName(null, "realpath"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("realpath " + path); } }) Interceptor.attach(Module.findExportByName(null, "realpath$DARWIN_EXTSN"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("realpath$DARWIN_EXTSN " + path); } }) Interceptor.attach(Module.findExportByName(null, "stat"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("stat " + path); } }) Interceptor.attach(Module.findExportByName(null, "statfs"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("statfs " + path); } }) Interceptor.attach(Module.findExportByName(null, "symlink"), { onEnter: function(args) { if (args[0].isNull()) return; var path = args[0].readUtf8String(); if (iswhite(path)) return; if (logtrace(this)) console.log("symlink " + path); } }) Interceptor.attach(Module.findExportByName(null, "syscall"), { onEnter: function(args) { if (args[0].isNull()) return; var callnum = args[0].toInt32(); if (callnum == 180) return; console.log("syscall " + args[0].toInt32()); } })
来源:https://www.cnblogs.com/lichao890427/p/12019676.html