8点击蓝字
关注我们
声明
本文作者:Jammny
本文字数:7326字
阅读时长:约10分钟
附件/链接:点击查看原文下载
本文属于【狼组安全社区】原创奖励计划,未经许可禁止转载
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,狼组安全团队以及文章作者不为此承担任何责任。
狼组安全团队有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经狼组安全团队允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
❝
通过 frida hook 来学习反越狱检测的通用方法和思路。
绕过敏感路径和文件检测
当越狱后会产生一些新的文件和应用,这些静态特征很容易被检测到。1、拦截C语言的stat函数,修改检测返回结果:
let jailbrokenPath = ["/Applications/Cydia.app","/usr/sbin/sshd","/bin/bash","/etc/apt","/Library/MobileSubstrate","/User/Applications/"];
let stat = Module.findExportByName("libSystem.B.dylib", "stat");
if (stat) {
Interceptor.attach(stat, {
onEnter: function(args) {
this.isCommonPath = false;
let pathname = args[0].readUtf8String();
for (let i = 0; i < jailbrokenPath.length; i++) {
if (pathname.indexOf(jailbrokenPath[i])>-1) {
log.debug(`Called stat(): ${pathname}`)
this.isCommonPath = true;
};
}
},
onLeave: function(retval) {
if (this.isCommonPath) {
retval.replace(-1);
}
}
});
};
2、拦截NSFileManager的fileExistsAtPath方法,修改检测返回结果:
resolver.enumerateMatches("-[NSFileManager fileExistsAtPath*]").forEach((matche) => {
Interceptor.attach(matche.address, {
onEnter: function(args) {
this.called = false;
this.pathname = ObjC.Object(args[2]).toString();
for (let i = 0; i < jailbrokenPath.length; i++) {
if (this.pathname==jailbrokenPath[i]) {
log.debug(`-[NSFileManager fileExistsAtPath:${this.pathname}] 0x1 => 0x0`);
try {
Interceptor.replace(matche.address, new NativeCallback((path) => {
return 0x0;
}, "int", ["pointer"]));
} catch(error) {
if (error!="Error: already replaced this function"){
log.error(error);
};
}
return
}
};
log.debug(`-[NSFileManager fileExistsAtPath:${this.pathname}] 0x1 => 0x0`);
},
});
})
绕过动态链接库检测
越狱后会产生一些以 .dylib 为后缀的动态链接库文件,比如说:/Library/MobileSubstrate/MobileSubstrate.dylib,这些静态特征很容易被检测到。拦截系统库函数 _dyld_get_image_name,并修改返回值的路径:
let jailbrokenPath = ["/Applications/Cydia.app","/usr/sbin/sshd","/bin/bash","/etc/apt","/Library/MobileSubstrate","/User/Applications/"];
let dyld = Module.findExportByName("dyld", "_dyld_get_image_name");
if (dyld) {
Interceptor.attach(dyld, {
onLeave: function(retval) {
let pathname = retval.readCString()
log.trace(`Called _dyld_get_image_name(): ${pathname}`);
for (let i = 0; i < jailbrokenPath.length; i++) {
if (pathname.indexOf(jailbrokenPath[i])>-1) {
let replace_path = ObjC.classes.NSString.stringWithString_("/System/Library/Frameworks/Intents.framework/Intents")
retval.replace(replace_path);
log.debug(`Replace ${pathname} => ${replace_path}`)
break
};
};
}
});
};
绕过越狱应用打开检测
oc可以通过canOpenURL方法以url链接的形式打开本地应用,比如说cydia://package/com.tigisoftware.Filza。只需要将canOpenURL的返回值修改为0即可绕过:
var urlPath = ['sileo', 'cydia']
let resolver = new ApiResolver("objc");
resolver.enumerateMatches("-[UIApplication canOpenURL:]").forEach((matche) => {
Interceptor.replace(matche.address, new NativeCallback((url) => {
return 0x0;
}, "int", ["pointer"]));
Interceptor.attach(matche.address, {
onEnter: function(args) {
this.called = false;
this.url = ObjC.Object(args[2]).toString();
log.trace(`-[UIApplication canOpenURL:${this.url}]`);
for (let i = 0; i < urlPath.length; i++) {
if (this.url.indexOf(urlPath[i])>-1) {
this.called = true
break
};
};
},
onLeave: function(retval) {
if (this.called) {
log.debug(`-[UIApplication canOpenURL:${this.url}] Replace 0x1 => 0x0`);
}
}
});
})
绕过私有写入权限检测
越狱前是没办法访问修改 /private 路径下的东西,通常使用使用 writeToFile:
方法将数据写入该路径下的文件中,来判断是否越狱。有得时候app可能需要写入一些缓存文件,属于正常业务,为了防止异常可以hook 返回的error信息:
var keyworld = ['Substrate', 'substrate', 'substitute', 'Substitrate', 'TweakInject', 'jailbreak', 'cycript', 'SBInject', 'pspawn', 'rocketbootstrap', 'bfdecrypt', 'frida', 'Shadow', 'AppsDump', 'TrollStore', 'TrollHelper', 'palera1n', 'sileo', 'cydia', 'Sileo', 'Cydia', '/etc/', '/private/']
let resolver = new ApiResolver("objc");
resolver.enumerateMatches("-[* writeToFile:atomically:encoding:error:]").forEach((matche) => {
Interceptor.attach(matche.address, {
onEnter: function(args) {
this.self = new ObjC.Object(args[0]), this.method = Memory.readUtf8String(args[1]),
this.tag = `[${this.self.$className} ${this.method}]`;
this.path = new ObjC.Object(args[2]).toString(), this.error = Memory.readUtf8String(args[5]);
if (this.error=="") {
Memory.writePointer(args[5], ObjC.classes.NSError.alloc());
log.debug(`${this.tag} write ${this.path} successfully. replace NSError.`)
} else {
log.trace(`${this.tag} write ${this.path} error.`);
}
},
onLeave: function(retval) {
if (this.error=="") {
for (let i = 0; i < keyworld.length; i++) {
if (this.path.indexOf(keyworld[i])>-1) {
log.debug(`${this.tag} write ${this.path} successfully. Replace 0x1 => 0x0`);
retval.replace(0x0);
break
};
};
};
}
});
})
绕过软链接检测
越狱后可能会生成一些软链接,lsta函数可以返回文件的属性,可以判断是否存在软链接。hook lstat函数的返回值为-1绕过:
let lstat = Module.findExportByName("libSystem.B.dylib", "lstat");
if (lstat) {
Interceptor.attach(lstat, {
onEnter: function(args) {
this.isCommonPath = false;
this.pathname = args[0].readUtf8String();
for (let i = 0; i < jailbrokenPath.length; i++) {
if (this.pathname==jailbrokenPath[i]) {
this.isCommonPath = true;
return
};
};
log.trace(`[lstat] ${this.pathname}`);
},
onLeave: function(retval) {
if (this.isCommonPath) {
log.debug(`[lstat] ${this.pathname} Replace ${retval} => -1`)
retval.replace(-1);
};
}
});
};
绕过越狱后的系统调用
有些系统函数或API在未越狱状态下,是不能使用的。比如说,未越狱的设备是不能用fork函数创建子进程的,其他类似fork的函数:posix_spawn,kill,popen等等。思路是hook这些越狱后才能调用的函数:
let fork = Module.findExportByName("Foundation", "fork");
if (fork) {
Interceptor.attach(fork, {
onLeave: function(retval) {
log.debug(`[dyld fork] Replace ${retval} => -1`)
retval.replace(-1);
}
});
};
绕过环境变量检测
越狱工具可以通过设置环境变量 DYLD_INSERT_LIBRARIES 来注入自己的动态库到应用程序中。通过hook环境变量检测的返回值达到绕过目的:
let getenv = Module.findExportByName("Foundation", "getenv");
if (getenv) {
Interceptor.attach(getenv, {
onEnter: function(args) {
this.env = Memory.readUtf8String(args[0]);
},
onLeave: function(retval) {
if (this.env=="DYLD_INSERT_LIBRARIES") {
log.debug(`[Foundation getenv] ${this.env} Replace ${retval} => 0x0`)
retval.replace(0)
} else {
log.trace(`[Foundation getenv] ${this.env}`)
}
}
});
};
绕过越狱弹窗提示
很多APP出于人性化考虑,当客户端检测到越狱环境的时候,通常会在UI顶层弹窗告诉用户该设备是越狱设备,比如说类似下图:一般情况下当弹窗结束后就会自动退出APP,虽然说这个设计很人性化,但是也造成了检测被绕过的风险。如果攻击者可以通过hook注入技术,让APP即使检测到越狱环境也不弹窗提示的话,那么就可以让APP一直保持运行状态,不会退出。
通常我们可以通过脱壳技术来获取APP客户端的二进制文件,使用IDA反编译搜索弹窗中出现的字符串内容,用于快速定位触发弹窗的函数或方法。比如说搜索字符串 “检测到设备越狱,即将退出程序”,可以直接定位到关键函数 sub_100131BA4(__int64 result, __int64 a2)
:
void __fastcall sub_100131BA4(__int64 result, __int64 a2)
{
__int64 v2; // x19
v2 = *(_QWORD *)(result + 32);
if ( qword_1043EEF60 == v2 )
return;
qword_1043EEF60 = *(_QWORD *)(result + 32);
if ( (v2 & 2) != 0 )
{
sub_100131854((__int64)CFSTR("检测到应用重签名,即将退出程序"), a2);
if ( (v2 & 4) == 0 )
{
LABEL_4:
if ( (v2 & 0x4000) == 0 )
goto LABEL_5;
goto LABEL_17;
}
}
else if ( (v2 & 4) == 0 )
{
goto LABEL_4;
}
sub_100131854((__int64)CFSTR("检测到设备越狱,即将退出程序"), a2);
if ( (v2 & 0x4000) == 0 )
{
LABEL_5:
if ( (v2 & 0x10) == 0 )
goto LABEL_6;
goto LABEL_18;
}
LABEL_17:
sub_100131854((__int64)CFSTR("检测到设备存在恶意屏蔽越狱的行为,即将退出程序"), a2);
if ( (v2 & 0x10) == 0 )
{
LABEL_6:
if ( (v2 & 0x20) == 0 )
goto LABEL_7;
goto LABEL_19;
}
LABEL_18:
sub_100131854((__int64)CFSTR("检测到MobileSubstrate动态库注入,即将退出程序"), a2);
if ( (v2 & 0x20) == 0 )
{
LABEL_7:
if ( (v2 & 0x40) == 0 )
goto LABEL_8;
goto LABEL_20;
}
LABEL_19:
sub_100131854((__int64)CFSTR("检测到逆向分析中常用的动态库,即将退出程序"), a2);
if ( (v2 & 0x40) == 0 )
{
LABEL_8:
if ( (v2 & 0x80) == 0 )
goto LABEL_9;
goto LABEL_21;
}
LABEL_20:
sub_100131854((__int64)CFSTR("检测到Frida/Cycript代码注入框架,即将退出程序"), a2);
if ( (v2 & 0x80) == 0 )
{
LABEL_9:
if ( (v2 & 0x100) == 0 )
goto LABEL_10;
goto LABEL_22;
}
LABEL_21:
sub_100131854((__int64)CFSTR("检测到调试器,即将退出程序"), a2);
if ( (v2 & 0x100) == 0 )
{
LABEL_10:
if ( (v2 & 0x400) == 0 )
goto LABEL_11;
goto LABEL_23;
}
LABEL_22:
sub_100131854((__int64)CFSTR("检测到反反调试,即将退出程序"), a2);
if ( (v2 & 0x400) == 0 )
{
LABEL_11:
if ( (v2 & 0x8000) == 0 )
goto LABEL_12;
LABEL_24:
sub_100131854((__int64)CFSTR("检测到设备存在指定函数被调试的行为,即将退出程序"), a2);
if ( (v2 & 0x1000) == 0 )
{
LABEL_13:
if ( (v2 & 0x10000) == 0 )
return;
LABEL_26:
sub_100131854((__int64)CFSTR("检测到资源文件被修改,即将退出程序"), a2);
return;
}
goto LABEL_25;
}
LABEL_23:
sub_100131854((__int64)CFSTR("检测到指定函数被hook,即将退出程序"), a2);
if ( (v2 & 0x8000) != 0 )
goto LABEL_24;
LABEL_12:
if ( (v2 & 0x1000) == 0 )
goto LABEL_13;
LABEL_25:
sub_100131854((__int64)CFSTR("检测到代码段被修改,即将退出程序"), a2);
if ( (v2 & 0x10000) != 0 )
goto LABEL_26;
}
定位到触发弹窗的函数后,我们可以根据实际情况选择修改还是替换它。通过情况下,我会优先选择直接置空这个触发弹窗的函数,frida replace 替换代码如下:
/**
* 获取函数内存地址
* @param {string} module 模块名称或路径
* @param {number} offset 偏移地址
* @returns {number} 返回函数在内存中的地址
*/
function get_func_addr(module, offset) {
let base_addr = Module.findBaseAddress(module); // 使用 Module.findBaseAddress 通过程序路径得到可执行程序在内存中的基地址
//console.log(`[base_addr:${base_addr}]`);
//console.log(hexdump(ptr(base_addr), {length: 16,header: true,ansi: true}))
let func_addr = base_addr.add(offset); // 再add其偏移地址得到在未导出函数在内存中的地址
if (Process.arch == 'arm')
// 如果是32位, 地址+1
return func_addr.add(1);
else
return func_addr;
}
/**
* 重写或替换原函数
* @param {number} func_addr 函数在内存中的地址
*/
function replace_sub_func(func_addr){
// NativeFunction 第二个参数表示返回值类型,第三个参数是个列表表示入参个数和类型
var fun = new NativeFunction(func_addr, 'void', ['int', 'int']);
Interceptor.replace(fun, new NativeCallback(function() {
console.log(`[${func_addr}] Bypass Detection.`)
}, 'void', ['int', 'int']));
}
// 本例中未导出函数在IDA中函数名为 sub_100131BA4(__int64 result, __int64 a2)
let func_addr = get_func_addr('iOS_Bank', 0x131BA4);
replace_sub_func(func_addr)
如果说你已经掌握了如何通过hook弹窗去绕过检测,那么再配合 Shadow 插件,我觉得已经可以应付大部分的APP了。
参考
iOS越狱检测app及frida过检测 – 『移动安全区』 – 吾爱破解 – LCG – LSG |安卓破解|病毒分析|www.52pojie.cn
iOS的越狱检测和反越狱检测剖析 – 简书 (jianshu.com)
系列文章
作者
Jammny
假如你坚持一个月做同一件事情
那么它很快会成为你的习惯,学习亦是如此。
扫描关注公众号回复加群
和师傅们一起讨论研究~
长
按
关
注
WgpSec狼组安全团队
微信号:wgpsec
Twitter:@wgpsec
原文始发于微信公众号(WgpSec狼组安全团队):IOS安全 APP 反越狱检测技术