IOS安全 APP 反越狱检测技术

admin 2024年1月26日10:07:41评论56 views字数 10046阅读33分29秒阅读模式
 

声明

本文作者: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顶层弹窗告诉用户该设备是越狱设备,比如说类似下图:IOS安全 APP 反越狱检测技术一般情况下当弹窗结束后就会自动退出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)

系列文章

IOS安全 测试环境搭建

IOS安全 webview hook 全局调试

IOS安全 SSL Pinning 单向证书检验

IOS安全 APP TLS 数据强制抓包与解包

IOS安全 APP 越狱检测技术

作者

IOS安全 APP 反越狱检测技术
Jammny

假如你坚持一个月做同一件事情

那么它很快会成为你的习惯,学习亦是如此。

 

原文始发于微信公众号(WgpSec狼组安全团队):IOS安全 APP 反越狱检测技术

 

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月26日10:07:41
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   IOS安全 APP 反越狱检测技术https://cn-sec.com/archives/2429944.html

发表评论

匿名网友 填写信息