今天说下怎么屏蔽iOS检测越狱思路

今天说下怎么屏蔽iOS检测越狱思路-清风博客

OC层面

首先可以尝试使用NSFileManager来判断设备安装了以下常用的越狱工具:

【ps】Zebra 斑马应用商店是基于Cydia或者Sileo的, 所以只要检测后2者即可

/Applications/Cydia.app
/Applications/Sileo.app
/Library/MobileSubstrate/MobileSubstrate.dylib
/bin/bash
/usr/sbin/sshd
/etc/apt
// 常见越狱文件
const char *Jailbreak_Tool_pathes[] = {
    "/Applications/Cydia.app",
    "/Library/MobileSubstrate/MobileSubstrate.dylib",
    "/bin/bash",
    "/usr/sbin/sshd",
    "/etc/apt"
};

char *printEnv(void){
    char *env = getenv("DYLD_INSERT_LIBRARIES");
    return env;
}

/** 当前设备是否越狱 */
+ (BOOL)isDeviceJailbreak
{
    // 判断是否存在越狱文件
    for (int i = 0; i < 5; i++) {
        if ([[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithUTF8String:Jailbreak_Tool_pathes[i]]]) {
            NSLog(@"此设备越狱!");
            return YES;
        }
    }
    // 判断是否存在cydia应用
    if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]]){
        NSLog(@"此设备越狱!");
        return YES;
    }
    
    // 读取系统所有的应用名称
    if ([[NSFileManager defaultManager] fileExistsAtPath:@"/User/Applications/"]){
        NSLog(@"此设备越狱!");
        return YES;
    }
    
    // 读取环境变量
    if(printEnv()){
        NSLog(@"此设备越狱!");
        return YES;
    }
    
    NSLog(@"此设备没有越狱");
    return NO;
}

但是不要写BOOL切换方法,会给 越狱工具 直接锁定目标hook绕过的机会

屏蔽越狱就是return NO;

+ (BOOL)isJailbroken {
   if ([[NSFileManager defaultManager] fileExistsAtPath:@"/Applications/Cydia.app"]){
    return YES;
   }
// ...
}

越狱工具可能会更改这些工具的安装路径,从而避来你的判断。

我们可以尝试打开Cydia或者Sileo应用注册的URL scheme:

if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]]){
   XLsn0wLog(@"Device is jailbroken");
}

但并非所有工具都会注册 URL scheme ,此外,攻击者可以修改任何应用程序的 URL scheme ,然后,其实我们可以尝试读取下应用列表,看看有无权限获取:

if ([[NSFileManager defaultManager] fileExistsAtPath:@"/User/Applications/"]){
    NSLog(@"Device is jailbroken");
    NSArray *applist = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"/User/Applications/"
                                                                           error:nil];
    NSLog(@"applist = %@",applist);
}

AppList 可以从越狱设备获取的:

今天说下怎么屏蔽iOS检测越狱思路-清风博客

攻击者可能会 hook NSFileManager 的方法,让你的想法变得不可取。

但是我们可以回避 NSFileManager,使用stat系列C函数检测Cydia等工具:

#import <sys/stat.h>

- void checkCydia(void)
{
struct stat stat_info;
if (0 == stat("/Applications/Cydia.app", &stat_info)) {
    NSLog(@"Device is jailbroken");
}
}

C层面

攻击者可能也利用 Fishhook 原理hook stat函数,自己可以看看stat是不是出自系统库,有没有被攻击者换掉:

 #import <dlfcn.h>

 -void checkInject(void) {
   int ret ;
   Dl_info dylib_info;
   int (*func_stat)(const char *, struct stat *) = stat;
   if ((ret = dladdr(func_stat, &dylib_info))) {
    NSLog(@"lib :%s", dylib_info.dli_fname);
   }
}

如果结果不是/usr/lib/system/libsystem_kernel.dylib,则100%被攻击。

如果 libsystem_kernel.dylib 被攻击者替换…其实屏蔽越狱就是更换libsystem_kernel动态库

你可能还会想,自己检索下自己的应用程序是否链接到异常动态库

可以列出所有已链接的动态库:

#import <mach-o/dyld.h>

 -void checkDylibs(void)
  {
    uint32_t count = _dyld_image_count();
     for (uint32_t i = 0 ; i < count; ++i) {
    NSString *name = [[NSString alloc]initWithUTF8String:_dyld_get_image_name(i)];
       NSLog(@"--%@", name);
     }
   }

一般情况下,越狱机器的输出会包含这样的字符串:Library/MobileSubstrate/MobileSubstrate.dylib。

目前越狱是基于 Comex 开发的 Substitute ,

今天说下怎么屏蔽iOS检测越狱思路-清风博客

攻击者可能会重命名 MobileSubstrate/Substitute,但不变的原理还是通过 DYLD_INSERT_LIBRARIES 注入动态库。

我们可以查看当前程序运行的环境变量:

-void printEnv(void)
{
  char *env = getenv("DYLD_INSERT_LIBRARIES");
  NSLog(@"%s", env);
 }

比如抖音的越狱检测

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());
    }
})

一般的话我们使用一些常规的防御性代码来做这种检测。 当然,这种检测还是有一定的误报概率,但是对于APP开发者来说,需要确定一个原理,即使越狱手机检测为未越狱,也不能将未越狱手机检测为越狱手机

检查stat是否来自系统库

  int ret;
  Dl_info dylib_info;
  int (*func_stat)(const char *,struct stat *) = stat;
  if ((ret = dladdr(func_stat, &dylib_info))) {
       if (strcmp(dylib_info.dli_fname,"/usr/lib/system/libsystem_kernel.dylib") != 0) { 
           jailbroken = YES; 
       }
}

检测程序运行的环境变量

//检测当前程序运行的环境变量,防止通过DYLD_INSERT_LIBRARIES注入链接异常动态库,来更改相关工具名称
 - (BOOL)checkEnv
{
    char *env = getenv("DYLD_INSERT_LIBRARIES");
    NSLog(@"%s", env);
    if (env) {
        return YES;
    }
    return NO;
}

绕过以下越狱检测

一、根据是否存在特定越狱文件以及特定文件的权限是否发生变化来判断设备是否越狱

二、根据是否安装ssh判断设备是否越狱

三、根据文件系统的分区是否发生变化来检测设备是否越狱

四、根据沙箱的完整性检查设备是否越狱


typedefint(*ptrace_ptr_t)(int_request,pid_t_pid,caddr_t_addr,int_data);void* dlopen(constchar* pathname,intmode );
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH31
#endif
voiddisable_gdb() {
    void* handle =dlopen(0,RTLD_GLOBAL|RTLD_NOW);
    ptrace_ptr_tptrace_ptr =dlsym(handle,"ptrace");
    ptrace_ptr(PT_DENY_ATTACH,0,0,0);
    dlclose(handle);
}
intmain(intargc,char* argv[]) {
#ifndef DUBUG
//    disable_gdb();
#endif

温馨提示:本文最后更新于2021-08-15 04:21:27,某些文章具有时效性,若有错误或已失效,请在下方留言或联系清风#
© 版权声明
THE END
文章不错?点个赞呗!
点赞336 分享
评论 共1条

请登录后发表评论