iOS越狱检测app及frida过检测

近期学习ios逆向,也为了熟悉一下iOS开发正向。用objective-c整了一个越狱检测的crackme,然后用frida过一遍自己写的检测。

iOS越狱检测app及frida过检测

准备


crackme app(https://github.com/andy0andy/ios_crackme)
◆ios 12.5.5
◆frida 14.0.0
克隆项目,再用xcode安装到手机上


使用stat检测敏感路径


利用stat检查一些越狱后才有的敏感路径,如:/Applications/Cydia.app/usr/sbin/sshd,以此来判断是否越狱。stat判断文件是否存在, 返回0则为获取成功,-1为获取失败。可通过hook stat,过掉检测。

function hook_stat(is_pass){
var stat = Module.findExportByName('libSystem.B.dylib', 'stat');
Interceptor.attach(stat, {
onEnter: function(args) {
// 这里是方法被调用时的处理逻辑
// args[0] 是 stat 方法的第一个参数,通常是文件路径
// args[1] 是 stat 方法的第二个参数,这里可以添加其他参数的处理
console.log('stat is hooked: ');
},
onLeave: function(retval){
if (is_pass){
retval.replace(-1);
console.log(`stat retval: ${Number(retval.toString())} -> -1`);
}
}
});
}

iOS越狱检测app及frida过检测

检查dylib是否合法


越狱后会产生一些特殊的链接库,ipa可以通过*_dyld_get_image_name*来获取所有的链接库,再遍历匹配,判断是否为越狱设备。

可以通过分析找到ipa检测的dylib,再hook_dyld_get_image_name,将返回替换为合法dylib,过掉检测。

function hook_dyld_get_image_name(is_pass){
let cheek_paths = [
"/Library/MobileSubstrate/MobileSubstrate.dylib",
]

let NSString = ObjC.classes.NSString;
let true_path = NSString.stringWithString_( "/System/Library/Frameworks/Intents.framework/Intents");



let _dyld_get_image_name = Module.findExportByName(null, "_dyld_get_image_name");
Interceptor.attach(_dyld_get_image_name, {
onEnter: function(args){

console.log("_dyld_get_image_name is hooked.")
this.idx = eval(args[0]).toString(10);

},
onLeave: function(retval){
let rtnStr = retval.readCString();

if(is_pass){
for (let i=0;i<cheek_paths.length;i++){

if (cheek_paths[i] === rtnStr.toString()){
retval.replace(true_path);
console.log(`replace: (${this.idx}) ${rtnStr} => ${true_path}`)
}
}

}

}
})

}

iOS越狱检测app及frida过检测

检测能否启动越狱app


越狱后会在手机上安装越狱设备,如cydia。可以通过-[UIApplication canOpenURL:]来检测是否能启动app。

可hook-[UIApplication canOpenURL:]替换返回过掉检测。但canOpenURL方法 返回是个BOOL,即YES/NO,也就是1和0的宏。但在Interceptor.attach里用 *retval.replace()*总是会导致app崩溃(不知道原理,望大佬指点)。

所以使用 Interceptor.replace() + NaviteCallback, 替换掉方法,使其固定返回 0,也就是 NO。但这个解法,也不能算是好方法。

function hook_canopenurl(is_pass){

let api = new ApiResolver("objc");
api.enumerateMatches("-[UIApplication canOpenURL:]").forEach((matche) => {

console.log("canOpenURL is hooked.");

if (is_pass){
Interceptor.replace(matche.address, new NativeCallback((url_obj) => {return 0;}, "int", ["pointer"]))
}
})


}

iOS越狱检测app及frida过检测

检测越狱文件和目录


越狱后会产生特殊的文件和目录,可以通过fileExistsAtPath来检测,直接hook过掉。

// -[NSFileManager fileExistsAtPath:isDirectory:]
function hook_fileExistsAtPath(is_pass){


let api = new ApiResolver("objc");
let matches = api.enumerateMatches("-[NSFileManager fileExistsAtPath:isDirectory:]")
matches.forEach((matche) => {

console.log("fileExistsAtPath is hooked.");

if(is_pass){
Interceptor.replace(matche.address, new NativeCallback((path, is_dir) => {
console.log(ObjC.Object(path).toString(), is_dir)
return 0;
}, "int", ["pointer", "bool"]))
}

})

}

iOS越狱检测app及frida过检测

检测是否可写私有路径权限


越狱后为root权限,可以在私有路径如/private/下创建文件。如果创建文件无异常则越狱,反之。

可通过ObjC.classes.NSError.alloc()构建一个异常写入ipa检测的异常指针中。

function hook_writeToFile(is_pass){

let api = new ApiResolver("objc");
api.enumerateMatches("-[NSString writeToFile:atomically:encoding:error:]").forEach((matche) => {

Interceptor.attach(matche.address, {

onEnter: function(args){
this.error = args[5];
this.path = ObjC.Object(args[2]).toString();
console.log("writeToFile is hooked");
},
onLeave: function(retval){
if(is_pass){
let err = ObjC.classes.NSError.alloc();
Memory.writePointer(this.error, err);
}
}

})

})

}

iOS越狱检测app及frida过检测

检测文件路径和是否是路径链接


越狱后有些文件会被移动,但这个文件路径又必须存在,所以可能会创一个文件链接。ipa可以检测一些敏感路径是否是链接来判断是否越狱。

这里仅过掉路径检测(符号链接不会过T.T)

// oc 检测函数
+ (Boolean)isLstatAtLnk{
// 检测文件路径是否存在,是否是路径链接
Boolean result = FALSE;

NSArray* jbPaths = @[
@"/Applications",
@"/var/stash/Library/Ringtones",
@"/var/stash/Library/Wallpaper",
@"/var/stash/usr/include",
@"/var/stash/usr/libexec",
@"/var/stash/usr/share",
@"/var/stash/usr/arm-apple-darwin9",
];

struct stat stat_info;

for(NSString* jbPath in jbPaths){
char jbPathChar[jbPath.length];
memcpy(jbPathChar, [jbPath cStringUsingEncoding:NSUTF8StringEncoding], jbPath.length);

if (lstat(jbPathChar, &stat_info)){
NSLog(@"stat_info.st_mode: %hu, S_IFLNK: %d, %d", stat_info.st_mode, S_IFLNK, stat_info.st_mode & S_IFLNK);
if(stat_info.st_mode & S_IFLNK){
result = TRUE;
NSLog(@"是路径链接>> %@", jbPath);
}
}else{
NSLog(@"路径不存在>> %@", jbPath);
result = TRUE;
}
}

return result;

}
// 过lstat
function hook_lstat(is_pass){
var stat = Module.findExportByName('libSystem.B.dylib', 'lstat');
Interceptor.attach(stat, {
onEnter: function(args) {

console.log('lstat is hooked: ');
},
onLeave: function(retval){
if (is_pass){
retval.replace(1);
console.log(`lstat retval: ${Number(retval.toString())} -> 1`);
}
}
});
}


检测fork


未越狱的设备是无法fork子进程。

hook fork

function hook_fork(is_pass){

let fork = Module.findExportByName(null, "fork");
if (fork){
console.log("fork is hooked.");
Interceptor.attach(fork, {
onLeave: function(retval){
console.log(`fork -> pid:${retval}`);
if(is_pass){
retval.replace(-1)
}
}
})
}

}

iOS越狱检测app及frida过检测

检测越狱常用的类


查看是否有注入异常的类,比如HBPreferences 是越狱常用的类,再用NSClassFromString判断类是否存在。

通过分析找出检测的类名,再去hookNSClassFromString。

function hook_NSClassFromString(is_pass){

let clses = ["HBPreferences"];

var foundationModule = Process.getModuleByName('Foundation');
var nsClassFromStringPtr = Module.findExportByName(foundationModule.name, 'NSClassFromString');

if (nsClassFromStringPtr){
Interceptor.attach(nsClassFromStringPtr, {
onEnter: function(args){
this.cls = ObjC.Object(args[0])
console.log("NSClassFromString is hooked");
},
onLeave: function(retval){

if (is_pass){
clses.forEach((ck_cls) => {

if (this.cls.toString().indexOf(ck_cls) !== -1){
console.log(`nsClassFromStringPtr -> ${this.cls} - ${ck_cls}`)
retval.replace(ptr(0x00))
}
})

}


}
})

}


}

iOS越狱检测app及frida过检测

检测是否有环境变量


通过getenv函数,查看环境变量DYLD_INSERT_LIBRARIES来检测是否越狱。

hook getenv

function hook_getenv(is_pass){

let getenv = Module.findExportByName(null, "getenv");

Interceptor.attach(getenv, {
onEnter: function(args){
console.log("getenv is hook");
this.env = ObjC.Object(args[0]).toString();
},
onLeave: function(retval){
if (is_pass && this.env == "DYLD_INSERT_LIBRARIES"){
console.log(`env: ${this.env} - ${retval.readCString()}`)

retval.replace(ptr(0x0))

}

}
})

}

iOS越狱检测app及frida过检测

整体代码


function hook_stat(is_pass){
var stat = Module.findExportByName('libSystem.B.dylib', 'stat');
Interceptor.attach(stat, {
onEnter: function(args) {
// 这里是方法被调用时的处理逻辑
// args[0] 是 stat 方法的第一个参数,通常是文件路径
// args[1] 是 stat 方法的第二个参数,这里可以添加其他参数的处理
console.log('stat is hooked: ');
},
onLeave: function(retval){
if (is_pass){
retval.replace(-1);
console.log(`stat retval: ${Number(retval.toString())} -> -1`);
}
}
});
}



function hook_dyld_get_image_name(is_pass){
let cheek_paths = [
"/Library/MobileSubstrate/MobileSubstrate.dylib",
]

let NSString = ObjC.classes.NSString;
let true_path = NSString.stringWithString_( "/System/Library/Frameworks/Intents.framework/Intents");



let _dyld_get_image_name = Module.findExportByName(null, "_dyld_get_image_name");
Interceptor.attach(_dyld_get_image_name, {
onEnter: function(args){

console.log("_dyld_get_image_name is hooked.")
this.idx = eval(args[0]).toString(10);

},
onLeave: function(retval){
let rtnStr = retval.readCString();

if(is_pass){
for (let i=0;i<cheek_paths.length;i++){

if (cheek_paths[i] === rtnStr.toString()){
retval.replace(true_path);
console.log(`replace: (${this.idx}) ${rtnStr} => ${true_path}`)
}
}

}

}
})

}


function hook_canopenurl(is_pass){

let api = new ApiResolver("objc");
api.enumerateMatches("-[UIApplication canOpenURL:]").forEach((matche) => {

console.log("canOpenURL is hooked.");

if (is_pass){
Interceptor.replace(matche.address, new NativeCallback((url_obj) => {return 0;}, "int", ["pointer"]))
}
})


}

// -[NSFileManager fileExistsAtPath:isDirectory:]
function hook_fileExistsAtPath(is_pass){


let api = new ApiResolver("objc");
let matches = api.enumerateMatches("-[NSFileManager fileExistsAtPath:isDirectory:]")
matches.forEach((matche) => {

console.log("fileExistsAtPath is hooked.");

if(is_pass){
Interceptor.replace(matche.address, new NativeCallback((path, is_dir) => {
console.log(ObjC.Object(path).toString(), is_dir)
return 0;
}, "int", ["pointer", "bool"]))
}

})

}



function hook_writeToFile(is_pass){

let api = new ApiResolver("objc");
api.enumerateMatches("-[NSString writeToFile:atomically:encoding:error:]").forEach((matche) => {

Interceptor.attach(matche.address, {

onEnter: function(args){
this.error = args[5];
this.path = ObjC.Object(args[2]).toString();
console.log("writeToFile is hooked");
},
onLeave: function(retval){
if(is_pass){
let err = ObjC.classes.NSError.alloc();
Memory.writePointer(this.error, err);
}
}

})

})

}

function hook_lstat(is_pass){
var stat = Module.findExportByName('libSystem.B.dylib', 'lstat');
Interceptor.attach(stat, {
onEnter: function(args) {

console.log('lstat is hooked: ');
},
onLeave: function(retval){
if (is_pass){
retval.replace(1);
console.log(`lstat retval: ${Number(retval.toString())} -> 1`);
}
}
});
}

function hook_fork(is_pass){

let fork = Module.findExportByName(null, "fork");
if (fork){
console.log("fork is hooked.");
Interceptor.attach(fork, {
onLeave: function(retval){
console.log(`fork -> pid:${retval}`);
if(is_pass){
retval.replace(-1)
}
}
})
}

}

function hook_NSClassFromString(is_pass){

let clses = ["HBPreferences"];

var foundationModule = Process.getModuleByName('Foundation');
var nsClassFromStringPtr = Module.findExportByName(foundationModule.name, 'NSClassFromString');

if (nsClassFromStringPtr){
Interceptor.attach(nsClassFromStringPtr, {
onEnter: function(args){
this.cls = ObjC.Object(args[0])
console.log("NSClassFromString is hooked");
},
onLeave: function(retval){

if (is_pass){
clses.forEach((ck_cls) => {

if (this.cls.toString().indexOf(ck_cls) !== -1){
console.log(`nsClassFromStringPtr -> ${this.cls} - ${ck_cls}`)
retval.replace(ptr(0x00))
}
})

}


}
})

}


}

function hook_getenv(is_pass){

let getenv = Module.findExportByName(null, "getenv");

Interceptor.attach(getenv, {
onEnter: function(args){
console.log("getenv is hook");
this.env = ObjC.Object(args[0]).toString();
},
onLeave: function(retval){
if (is_pass && this.env == "DYLD_INSERT_LIBRARIES"){
console.log(`env: ${this.env} - ${retval.readCString()}`)

retval.replace(ptr(0x0))

}

}
})

}



setImmediate(() => {
hook_stat(true);
hook_dyld_get_image_name(true)
hook_canopenurl(true);
hook_fileExistsAtPath(true);
hook_writeToFile(true);
hook_lstat(true);
hook_fork(true);
hook_NSClassFromString(true);
hook_getenv(true)

})


小结


检测的正向代码在项目的JailBreakCheek类下。单独过这些检测基本没啥难度,直接hook。但在真实app中还是重点在分析中,如何找到这些具体检测的点。

文章参考


ios反越狱检测与检测剖析
(https://www.jianshu.com/p/65b4929f26ce)

Project: iOS Jailbreak Bypass
(https://codeshare.frida.re/@incogbyte/ios-jailbreak-bypass/)


iOS越狱检测app及frida过检测


看雪ID:andyhah

https://bbs.kanxue.com/user-home-928251.htm

*本文为看雪论坛优秀文章,由 andyhah 原创,转载请注明来自看雪社区

iOS越狱检测app及frida过检测


# 往期推荐

1、Large Bin Attack学习(_int_malloc源码细读 )

2、CVE-2022-2588 Dirty Cred漏洞分析与复现

3、开发常识 | 彻底理清 CreateFile 读写权限与共享模式的关系

4、XAntiDenbug的检测逻辑与基本反调试

5、Frida-Hook-Java层操作大全

6、符号执行去除BR指令混淆


iOS越狱检测app及frida过检测

iOS越狱检测app及frida过检测

球分享

iOS越狱检测app及frida过检测

球点赞

iOS越狱检测app及frida过检测

球在看



iOS越狱检测app及frida过检测

点击阅读原文查看更多

原文始发于微信公众号(看雪学苑):iOS越狱检测app及frida过检测

版权声明:admin 发表于 2024年4月1日 下午6:02。
转载请注明:iOS越狱检测app及frida过检测 | CTF导航

相关文章