type
status
date
slug
summary
tags
category
icon
password
主要目的:虚拟定位打卡
1. 过环境
在越狱的iOS设备上运行,发现总是崩溃。第一步,肯定是要过反越狱了。先看一下崩溃日志。

崩溃发生在BBSCRUtility这个framework中,莫名其妙的调用了一个未知的位置,导致崩溃。
BBSCRUtility是什么呢,经过一番搜索。

但并没有现成的分析。直接拖进IDA。

函数名和类名被大量混淆。这里直接看load函数,因为加载framework的时候一定会被执行,还有modinit函数。


比较好是函数名还是能看到的。先看startup函数。

ssh上手机直接运行可执行文件,每次都输出scup clearance后崩溃。正好在start内看到NSLog,而且后面接了个JUMPout。if条件的两个函数,其中一个名称被混淆,另外一个可见是asm_jb_check。猜测二者都是越狱检测。

jumpout这块,看一下汇编,可以发现是根据错误原因,跳转到不同位置进行执行。崩溃方法都是一致的,就是跳转到0xB5A0X000来执行,一下就访问非法内存崩溃了。故接着直接搜索这个0xB5A00000,看看还有哪里用到了。

发现一个专门用来处理错误的函数scup_clearance以及一堆hook检测。#

1.1 初始化和检测内容
先看load函数。


除了检测越狱,还有重签名检测、frida检测、AirPlay检测、Debug检测、SwizzlingHook检测、InlineHook检测、AppIntegrity检测、SslPinning检测、NetworkProxy检测、FishHook检测、LocationFakeCheck、SimulatorCheck
除此之外,是在函数开头有这样一个内容,尽管这里并没启用,但我们还是可以分析一下:

其中:

猜测这里的check_nametab应该是一个修复可执行文件的表。其中包含了若干个如下的结构。
至于为什么没有启用,我认为是受到代码签名的影响。非越狱手机上,动态的对macho文件进行修改的话,会直接导致崩溃。
除此之外,还有一个beforeload函数。

这个is_bind_enable只是检测全局变量init_tab是否等于0xa985。如果是则返回1。接着通过调用
task_info
获取当前进程的信息,其中 0x11
是 TASK_DYLD_INFO
,返回当前进程中 dyld(动态链接器)的相关信息。task_info_out
返回的是一个 task_dyld_info
结构体的指针:- 第一个成员是
all_image_info_addr
,这是一个dyld_all_image_infos
结构体的地址。
- 它接下来从该地址中读取模块总数(v5)和模块数组地址(v4)。遍历所有已加载模块,尝试在模块路径中查找匹配的名字。
v3[1]
是模块的路径字符串,strrchr
提取出文件名部分,和byte_E4690
(应该是目标模块名)进行比较。若匹配,则保存模块的某个地址(v7 = *v3;
)并退出循环,对该模块的某个偏移地址写入一个值(魔数0xA895
)。
个人感觉看下来应该是方便和其他模块联动,比如在主程序中得知这个模块的加载状态。
完成之后,就是去拿配置了。

是构造了一个主bundle路径,也就是ipa安装后的.app的位置,上里面去找一个叫.conf.bbscr的文件。ls一下果然是存在的。


拿到文件后AES解密,解密密钥为bbscrbbscrbbscrx。进一步找解密函数,这里传入IV。

解一下
那这里又找到单点失效的问题了,只要是这个文件被篡改了就能把所有东西都关了。
1.2 越狱检测1:asm_jb_check
这里使用了大量的syscall。

最前面的数组,也看一下,发现是一些越狱的特征的路径

列个表:
系统调用 | syscall编号 | 作用 |
syscall(33, ...) | 33 | 应该是access
syscall(0, 33); // 等价于 syscall(33) |
access(path, 0) | SYS_access | 检查文件是否存在 |
lstat64(path, ...) | SYS_lstat64 | 获取符号链接信息(不跟随符号链接) |
stat64(path, ...) | SYS_stat64 | 获取文件状态(跟随符号链接) |
statfs64(path, ...) | SYS_statfs64 | 获取文件系统信息(如挂载点、磁盘类型等) |
open(path, 0, 0) | SYS_open | 尝试打开文件(只读) |
1.3 if的另一个条件函数(也是jb检测)

调用了一大堆被混淆了名字的函数,这里一个个看一下。
mEvHucLJUnMaXnco tEAXUmRFDcarnHWG:路径检测
检测这些路径是不是存在
mEvHucLJUnMaXnco qNygxgxIkrLgTUUq:函数绑定检测
检查系统中 _stat 和 _lstat 两个函数实际绑定的动态库是否为 /usr/lib/system/libsystem_kernel.dylib,如果不是,则记录下它们对应的库路径。
- 准备一个可变数组
NSMutableArray
:
- 检查
_stat
函数的绑定: - 构造一个包含两个键值对的
NSDictionary
: - 把这个字典加到前面的数组中。
如果能获取
_stat
的符号信息,并且 v11.dli_fname
(其所在的动态库路径)不是 /usr/lib/system/libsystem_kernel.dylib
,则说明被 hook 或替换了。- 对
_lstat
做同样的检查并记录:
和上面几乎一样,只是把名字换成了
"lstat"
。- 最后返回这个数组作为结果:
mEvHucLJUnMaXnco dyBCqojPEWqkidwU:动态库注入检测
检测当前进程环境变量中是否设置了
DYLD_INSERT_LIBRARIES
,并判断其中的值是否为非系统库注入(即不是以 /usr/lib/lib
开头),如果是,则记录下来并返回一个包含可疑注入信息的数组。1. 准备检测目标:
要检测的环境变量是:
DYLD_INSERT_LIBRARIES
—— 这是 macOS 动态库注入的主要机制。2. 获取当前进程的环境变量字典:
3. 遍历数组
[DYLD_INSERT_LIBRARIES]
:注意这句判断逻辑:
所以是筛选出非系统库路径的 DYLD 插入项。
4. 可疑项以字典形式加入结果数组中:
5. 返回最终 NSMutableArray 结果:
mEvHucLJUnMaXnco luDundrjxNAOKXUk:检测系统中某些关键目录是否被替换成了符号链接

mEvHucLJUnMaXnco RzNOPTOZAlkQxNmQ
在当前进程中检查是否存在已知的Hook相关函数符号(symbol)

[mEvHucLJUnMaXnco gGMoJRCRwptQCJqD]
检测当前进程中是否存在越狱/注入相关的动态库或模块
它的作用是检测是否有动态库或段名中包含类似
"substrate"
, "libhooker"
, "FlyJB"
这类越狱相关的关键词。✅ 检查已加载的动态库名中是否包含越狱特征字符串 | 使用 _dyld_get_image_header() + dladdr() |
✅ 分析 Mach-O segment 中的内容是否异常 | 使用 task_info() 获取 DYLD 映像信息,查找是否有篡改或加载非系统库 |
mEvHucLJUnMaXnco dsajOWCycDceJPML
这个函数的主要功能是检查进程的环境变量中是否包含特定的字符串:
_MSSafeMode
或 substitute
,这些字符串通常出现在越狱检测、安全模式判断或Tweak注入判断中。- 初始化目标字符串列表:
这些是常见于被注入Tweak或安全模式时的环境变量关键词。
- 遍历
_NSGetEnviron()
返回的环境变量(char ***environ
)列表: - 每一个环境变量通过
stringWithUTF8String:
转成NSString
。 - 然后用
containsString:
和location
数组中的每个目标关键词比对。 - 如果匹配到了任何一个目标关键词,就设置标志位
= 1
。
- 再次遍历
environ
全局变量(可能是冗余的,但加强检测): - 与前面类似,对每一项再次尝试包含性检查。
这个函数的作用大概率是检测当前进程环境中是否出现了
_MSSafeMode
或 substitute
这类越狱或Tweak注入相关的字符串,以此判断自己是否运行在受控环境中,例如:- iOS 越狱环境
- 被 Tweak 注入或 Hook
- 安全模式下运行
1.4 frida检测
1.socket检测

这里其实是A269,也就是frida默认端口号27042
2.扫描frida

这里开了个线程一直扫frida。字符串是简单的后移一位。


恢复出来是
- 遍历系统中的进程或内存:
- 使用
_dyld_image_count()
来获取当前加载的进程数量,然后通过_dyld_get_image_header()
遍历每个进程的内存映像。 - 在遍历每个映像时,检查它们的内存区域是否包含某些特征字符串,例如“/System/Library/”,是通常在 Frida 所注入的内存中可能出现的路径。
- 检查特定的 CPU 类型和加载的内存区域:
- 在每个映像的
ncmds
指令中,检查cputype
是否为特定值(13),并查找与 Frida 相关的特征。 strstr
函数用于查找内存区域中是否包含与 Frida 相关的字符串。
- 读取并分析内存内容:
- 使用
vm_region_64
和vm_read_overwrite
来读取内存内容并存入v27
中。这部分代码尝试读取映像中的内存并搜索是否有与 Frida 标志相匹配的内容。 - 对于每个内存区域,代码会使用多个字符串比对方法(包括大小写无关的比较)来检测是否存在 Frida 的标志。
- 检测过程:
- 如果在内存中找到匹配的字符串(如 Frida 的标志),就认为该进程可能正在运行 Frida。
- 代码会对每个可能的匹配区域执行进一步的验证,确保字符串是否在内存中出现,从而确认是否是 Frida 的注入。
- 内存分析:
- 在检测到潜在的 Frida 注入时,代码会通过内存区域复制和比对来进一步检查 Frida 是否已经在目标进程中加载。
- 使用各种内存操作并进行大小写无关的字符串比对,检测 Frida。
代码太长,有兴趣可以研究一下
然后在回调函数中又是熟悉的崩溃。

1.5 重签名检测:teamid和bundlename
首先重点关注这个函数,它的目的是在Mach-O文件的
__LINKEDIT
段中搜索某个Team ID字符串(a1
参数指定)是否存在。做法是获取当前可执行镜像的Mach-O头和虚拟地址偏移,遍历所有的load command,- 如果找到了类型为
LC_SEGMENT_64
(即p_cputype == 25
),并且段名是"__LINKEDIT"
,则保存这个段的fileoff
和filesize
。
- 如果找到了类型为
LC_CODE_SIGNATURE
(即p_cputype == 29
),则保存下来(这条命令包含代码签名的位置和大小)。
- 如果没有找到
__LINKEDIT
或LC_CODE_SIGNATURE
,就提前返回1(默认为成功或跳过):
- 根据
__LINKEDIT
和LC_CODE_SIGNATURE
的信息计算签名数据在内存中的实际地址v3
:
- 检查签名的Magic数是否匹配:
- 这四个字节
0xFA 0xDE 0x0C 0xC0
是苹果代码签名的魔数(CSMAGIC_EMBEDDED_SIGNATURE
)。
- 在签名区域中搜索字符串
a1
: - 如果找到了 Team ID 字符串,返回1,并打印日志
Scriosprotectt_7
。 - 否则返回0,并打印日志
Scriosprotectt_6
。
然后看它的调用者:
判断传入的 Team ID 字符串(参数
a3
)是否和系统当前签名的 Team ID 前缀匹配。📌 函数作用总结:- 如果参数
a3
(一个NSString
类型)为空: - 打印日志
Scriosprotectt
,返回true
(v17 = 1
)。
- 如果
a3
不为空,调用checkTeamID(a3.UTF8String)
: - 如果这个字符串已经在代码签名中被包含,直接返回
true
,跳过后续校验。 - 否则,进行真正的 系统签名 Team ID 提取和比对。
那么可以理解成代码应该是这样:
除了teamid之外,还有bundleName的检测。这个函数是判断当前和传入的一不一样
化简后其实很简单了
1.6 AirPlay检查
第一个函数,检查当前音频输出端口是否为 AirPlay 或 HDMI,如果是则返回该端口名称(如 "Apple TV" 或 "HDMI"),否则返回 nil。
第二个函数,如果刚才检测出来的端口名称不是nil(代表音频在AirPlay播放),否则检查iOS是否大于11.0并且通过
-[UIScreen isCaptured]
来判断是否正在AirPlay。最后还是熟悉的崩溃方式。1.7 debugcheck

检测函数jnndIJCDUPkbiIxp
这是设置 sysctl 查询的 MIB(Management Information Base)数组:
v6[0] = 1
→CTL_KERN
,表示查询内核信息。
v6[1] = 14
→KERN_PROC
,表示查询进程信息。
v6[2] = 1
→KERN_PROC_PID
,表示根据进程 PID 查询。
v6[3] = getpid()
→ 当前进程的 PID。
由于
v4
是 BYTE v4[32]
,很可能只是完整 kinfo_proc
结构体(实际上应该是更大),这里被简化了。在 macOS 和 iOS 中,
kinfo_proc
的结构里有个成员叫 kp_proc.p_flag
,表示进程状态标志:也就是说:
实际上是检查
P_TRACED
位是否被设置。1.8 SwizzlingHookCheck
先来了解一下Method Swizzling的原理:在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,
我们可以利用 class_replaceMethod 来修改类,
我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
……
归根结底,都是偷换了selector的IMP,如下图所示:

例如:
类方法如何 Swizzle?
必须获取 元类(Meta-class) 才能操作类方法:
注意这里虽然用的是
class_getClassMethod
,它其实也是查 metaClass
的方法表。元类(Meta-Class)是什么?简单来说,元类是类的类。每个类本身就是一个对象,负责存储实例方法(instance methods),而元类负责存储类方法(class methods)。- 类对象:定义了实例的属性和行为(实例方法)。
- 元类对象:定义了类本身的属性和行为(类方法)。
在 Objective-C 中:
方法类型 | 属于哪类对象 | 替换方式 |
实例方法 | 实例的 isa 指针指向的类(Class) | class_getInstanceMethod |
类方法 | 类对象的 isa 指针指向的“元类”(Meta-Class) | 需要操作元类 |
那如何进行检测呢?
首先,遍历所有该进程中加载的模块,遍历该模块 image 路径中是否包含系统框架路径,如果包含,则继续。获取指定 Mach-O 文件中的所有类名; 遍历每一个类名;忽略包含
.
的类名; 获取类和元类对象,然后调用了这样一个闭包v34 = objc_retainBlock(&__block_literal_global_0);
下面是这个闭包的invoke,本质上是遍历给定类(或元类)的所有方法,并将其封装成自定义对象添加到数组中。
完成了以上的工作:遍历所有类 → 封装每个类的方法 → 加入
__SCRMethodWapper
实例中。就可以进行swizzling hook检测了。
下面就是判断
__SCRMethodWapper
封装的方法是否被 swizzling 或被注入的动态库 hook。使用
v15
(即方法指针)来获取该方法的实际实现(IMP
);使用 dladdr
函数来获取该方法实现所在的动态库信息,存储在 v22
中;- 如果能读取出来,并且不是<redacted>
- 判断该方法实现是否来自系统库(例如
/System/Library/Frameworks
)。如果是_objc_msgForward
或__trampolines
相关的名称,则认为该实现是动态方法或转发方法,设置v25 = 0
表示不是hook - 检查方法实现所在的动态库是否是当前主程序(通过
dyld_get_image_name(0)
获获取),如果是,设置v25 = 0
; - 检查是否就是当前framework,不是视为被hook。
- 如果
dli_fname
是/Library/MobileSubstrate/DynamicLibraries
,这表明动态库来自 MobileSubstrate 直接视为被hook。
那么检测的整体流程如下:
🔹 第一步:类方法收集 + 包装
你之前看到的逻辑大致是:
- 遍历所有类(
objc_getClassList
);
- 获取每个类的方法;
- 把每个方法封装成一个
__SCRMethodWapper
对象;
- 保存进一个数组或容器中。
🔹 第二步:对每个
__SCRMethodWapper
调用检测函数- 也就是你现在看到的这段检测函数。
- 每个
__SCRMethodWapper
都有一个关联的method
,这个检测函数就对它的 IMP 做dladdr
检查,看是否被 swizzle/hook。
1.9 InlineHookCheck
inline hook的检测,建立在刚才遍历所有方法的基础上。

根据架构读取不同的文件。然后调用下面这个函数。从里面提取数据,将提取的数据传递给函数
schedule_check_all_func
。接着来看这个函数,知识简单的检查指令格式。
除此之外,还有另一个inline hook检查,不过这个仅用来检查是不是定位相关的函数被hook了。
主要做了以下几个工作:
解密字符串,判断路径是否存在;
检查类方法IMP是否被Hook,这里是通过检查指令机器码实现的。
获取线程名以判断是否运行在 Hook 工具的线程中。
1.10 AppIntegrityCheck
从如下代码来看,逻辑并不复杂。找到当前bundle下的文件,读取
.conf.ibcx
,经过解密后拿到一个带cputype的md5字符串,这个会作为目标值进行验证。当前text段段checksum来自函数SCRGetTextSegmentOfMemory(v12, v13, (__int64)calculated_checksum);
接下来看这2个函数,SCRGetTextSegmentOfMemory。找text段,计算checksum。计算了md5而已。然后和目标对比是否一致。
1.11 checkNetworkProxy
这里比较简单,从CFNetworkCopySystemProxySettings获取了两个值
kCFNetworkProxiesHTTPProxy, kCFNetworkProxiesHTTPPort
1.12 Fishhook check
首先需要了解一下fishhook的基本原理。
fishhook 的实现原理涉及到四个「表」
- 符号表(Symbol Table)
- 间接符号表(Indirect Symbol Table)
- 字符表(String Table)
- 懒加载和非懒加载表(__la_symbol_ptr/__non_la_symbol_ptr)
其中符号表(Symbol Table)与字符表(String Table)在 LC_SYMTAB 类型的 Load Command 中描述。

nlist_64
的第一个成员 n_un
代表当前符号的名称在字符表(String Table)中的相对位置。间接符号表(Indirect Symbol Table)在
dysymtab_command
结构体的 Load Command(类型为LC_DYSYMTAB)中描述。懒加载与非懒加载表位于
__DATA/__DATA_CONST
segment 下面的 section 中。- 当前可执行文件或动态库引用外部动态库符号时,调用到对应符号时会跳转到懒加载与非懒加载表指定的地址执行
- 懒加载表在符号第一次被调用时绑定,绑定之前指向桩函数,由桩函数完成符号绑定,绑定完成之后即为对应符号真实代码地址
- 非懒加载表在当前 Mach-O 被加载到内存时由 dyld 完成绑定,绑定之前非懒加载表内的值为 0x00,绑定之后同样也为对应符号的真实代码地址
fishhook 的作用就是改变懒加载和非懒加载表内保存的函数地址
由于懒加载和非懒加载表没有包含任何符号字符信息,我们并不能直接通过懒加载表和非懒加载表找到目标函数在表中对应的位置,也就无法进行替换。因此,需要借助间接符号表(Indirect Symbol Table)、符号表(Symbol Table)、字符表(String Table)三个表之间的关系找到表中与之对应的符号名称来确认它的位置。

从上图可以很清楚的看出,首先去懒加载表找对应index,找到后读出来去间接符号表再找,读出来之后去符号表,就能找出函数地址和字符串索引。修改前者来进行hook,后者就是函数名。
fishhook 所做的就是通过在懒加载表与非懒加载表中找到与目标函数名匹配的外部函数,一旦发现匹配则将其地址改为自定义的函数地址。也就是说,根据算好的符号表地址和偏移量,找到在符号表中用于指向共享库目标函数的指针,将该指针的值(即目标函数的地址)赋值给
rebinding
的*replaced
,最后修改该指针的值为replacement
(新的函数地址) 这里研究的fishhook通过这个函数检测常用系统函数是否被fishhook了,后触发回调。
该函数依次调用
fish_hook_check_c
检查以下函数的地址是否与预期库一致:getenv
是否来自libsystem_c.dylib
open
是否来自libsystem_kernel.dylib
_sysctl
是否来自libsystem_c.dylib
_dlsym
是否来自libdyld.dylib
_syscall
是否来自libsystem_kernel.dylib
下一步分析fish_hook_check_c函数,这一函数逐步判断
a1
是否仍在指定动态库的 .text
段范围内。a1
是否在主程序 Mach-O 的.text
段中;
- 如果不是,再检测它是否在
a2
指定的动态库的.text
段中;
- 若都不是,可能被 Hook(通过 fishhook 修改了符号跳转)。
1.13 LocationFakeCheck
2. 虚拟定位
3. 反虚拟检测
在IDA中频繁发现字符串
CDVPlugin
。经搜索得知这是 Apache Cordova(也叫 PhoneGap) 框架中的一个核心类,用于开发 原生插件,使得 JavaScript 可以调用原生 iOS 功能。经过一番尝试和猜测,发现,整个app的ui果然如预期,是js写的,也就是webkit。 并且,APP首次启动会下载一个超大的更新包,最终在
/var/mobile/containers/data/Application/xxxx/Library/NoCLoud
内,发现了www文件夹,文件夹很大,200多MB,拖下来全是源码。
在VSCode中搜索字符串,找到了几个相关的。

又根据打卡的字符串,确定js文件是morningEvent。一样的拿出来用python写个小脚本给格式化一下。
接下来去找哪里弹出了这个错误弹窗。

是alert,正好上方,是发送请求的代码。于是看到:
因为我们只改了经纬度,不会出现什么异常,那么唯一有问题的就是
this.fictitious
所以,再搜是哪里给这个赋值的
就是这个e.sent,但是e是啥呢。
在这段代码中,
e
是一个函数,通常代表一个 generator function 的上下文对象,它是在 Object(u["a"])().wrap
中被创建的,作为异步流程控制的一部分。具体来说,e
在此作为 generator 的内部变量,负责控制 yield
和 return
的执行流。e.sent
代表的是上一次 yield
操作返回的值。你可以看到代码中,e.sent
被赋值给 t.fictitious
,这意味着 t.fictitious
的值来源于 Object(T["d"])()
执行的结果,即 e.sent
会接收到 Object(T["d"])()
返回的值。因此,
e
作为 generator 函数的上下文对象,在异步调用时充当了一个控制流的角色。当 Object(T["d"])()
返回一个 Promise,并且这个 Promise 被 resolved 时,e.sent
就会接收到返回的结果。要了解
e.sent
的具体值,你需要查看 Object(T["d"])()
的实现,并了解它返回的是什么。通常,它可能是一个异步请求的返回数据,或者是一个某个操作的结果。那么就得知道T是什么。搜索后得到
T = a("ef3f"),
继续搜索,然后是这样的,还记得我们要找d吗
非常清楚,要找的
d
对应的是:这意味着:
T.d
就是 r
—— 我们现在的目标就是:找出这个模块里 r
是什么函数。然后找到:
现在要追踪的
General.isInstallAISIAide()
是一个外部调用 —— 它不是当前模块里定义的函数,而很可能是:来自宿主 App 中注入的 JS bridge 接口(比如 WebView 里注入的对象)
这里很简单的是,在ida内搜函数名就搜到了。

这里请求了一系列的url,判断是不是虚拟环境。其实还是我们设备异常的问题,并非修改了定位导致的。

分别是这些。
只要给其中结果patch成0,就行了。patch完后如下。


以前这个地方是结果变量,现在变成0了。
- Author:Lynnette177
- URL:https://next.lynnette.uk/article/jailbreak_check
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts