在本文中,用到的工具有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>::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>::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,std::__1::allocator>::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>::~basic_streambuf(&v40);
std::__1::basic_iostream>::~basic_iostream(&v37, off_206CBF8);
std::__1::basic_ios>::~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
抖音的越狱检测分析到此为止
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());
}
})