吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 13057|回复: 112
收起左侧

[iOS 原创] iOS越狱检测app及frida过检测

  [复制链接]
andyhah 发表于 2023-6-6 21:26

简介

近期学习ios逆向,也为了熟悉一下iOS开发正向。用objective-c整了一个越狱检测的crackme,然后用frida过一遍自己写的检测(真是没事找事)。代码粗糙,请见谅。

准备

克隆项目,再用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`);
      }
    }
  });
}

检查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}`)
          }
        }

      }

    }
  })

}

检测能否启动越狱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"]))
    }
  })

}

检测越狱文件和目录

越狱后会产生特殊的文件和目录,可以通过 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"]))
    }

  })

}

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

越狱后为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);
        }
      }

    })

  })

}

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

越狱后有些文件会被移动,但这个文件路径又必须存在,所以可能会创一个文件链接。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)
        }
      }
    })
  }

}

检测越狱常用的类

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

通过分析找出检测的类名,再去hook NSClassFromString

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

        }

      }
    })

  }

}

检测是否有环境变量

通过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))

      }

    }
  })

}

整体代码

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中还是重点在分析中,如何找到这些具体检测的点。这次分享的frida代码有点粗糙,啊哈哈,要实际使用还得再优化一下。并且可以多看看frida官方的脚本网站https://codeshare.frida.re/

后面有时间的话,再分享些其他类型的检测如frida检测,混淆代码或加固之类。

文章参考

免费评分

参与人数 20吾爱币 +20 热心值 +18 收起 理由
junjia215 + 1 + 1 用心讨论,共获提升!
eric + 1 + 1 谢谢@Thanks!
mzx699 + 1 + 1 我很赞同!
272607452 + 1 + 1 我很赞同!
imued12 + 1 + 1 我很赞同!
Danvi787 + 1 鼓励转贴优秀软件安全工具和文档!
RippleSky + 1 谢谢@Thanks!
N1san + 1 + 1 热心回复!
qwq5555 + 1 + 1 我很赞同!
2016976438 + 2 + 1 谢谢@Thanks!
lZEROl + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
7cn★文 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
李佑辰 + 3 + 1 我很赞同!
抱歉、 + 1 用心讨论,共获提升!
三年i + 1 + 1 大佬,能安排一下交通银行越狱屏蔽吗,试过很多了,一直过不了检测,直接闪.
lover1989 + 1 我很赞同!
DSUPER + 1 + 1 谢谢@Thanks!
gamingnow + 1 + 1 用心讨论,共获提升!
timeni + 1 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

qlnz 发表于 2023-6-8 11:04
渣打银行香港   这个app是直接打开就跳网页,重启手机到未越狱状态(未清除越狱环境) 依然进不去,直接跳网页,很难搞。
tianruo1987 发表于 2023-6-7 15:41
有个ipa,安装在macbook M1的 playcover中,打开提示设备已越狱,直接在macbook上安装,打开也是提示设备已越狱,怎么破?
pb297110281 发表于 2023-6-7 12:30
linchurong888 发表于 2023-6-7 13:13
支持支持,谢谢分享
sdieedu 发表于 2023-6-7 13:43
很不错的技术
wzbAwxl 发表于 2023-6-7 14:42
感谢感谢
jmfabc 发表于 2023-6-7 15:11
厉害了楼主
雨落惊鸿, 发表于 2023-6-7 15:59
感谢分享
zuq001 发表于 2023-6-7 15:59
以前一直很好奇某些app怎么检测越狱,谢楼主了
头像被屏蔽
大罗金仙 发表于 2023-6-7 16:46
提示: 该帖被管理员或版主屏蔽
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-29 12:59

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表