首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
iOS安全
发新帖
7
10
[原创]iOS安全开发中的Frida检测
发表于: 2025-12-31 13:28
3535
[原创]iOS安全开发中的Frida检测
三六零SRC
2025-12-31 13:28
3535
> 作者:luoyanbei@360src 随着移动应用安全机制的不断加强,以 Frida 为代表的动态插桩工具,已成为移动端逆向分析的主流手段。相比反汇编、class-dump 等静态分析方式,Frida 几乎可以在不修改 App 本体的情况下,实时注入代码、Hook 任意函数、篡改参数与返回值,甚至直接操控业务逻辑执行流程。 在越狱的iOS设备上,Frida 可以通过 frida-server 以最高权限运行;在非越狱设备上,也可以通过 Frida Gadget、调试注入、重签名等方式完成动态分析。 在实际攻击场景中,Frida 已被广泛用于: • 绕过登录、风控、反作弊等安全校验 • Hook 网络层以抓取或篡改敏感接口数据 • 直接调用内部私有方法,伪造正常业务流程 • 分析加密算法、关键参数生成逻辑 • 对安全检测逻辑本身进行反制与绕过 在 iOS App 中系统性地识别 Frida 的运行痕迹、注入特征和行为特征,并构建多层次的防御与对抗策略,已经成为移动端安全中不可回避的一环。本文将结合实际逆向与对抗经验,从安全开发的角度讲解frida检测相关特征。 ## 1、检测frida默认端口号 如果设备上运行了 frida-server,并且使用默认端口号27042,App 尝试连接 127.0.0.1:27042,连接成功 说明当前环境下存在Frida使用27042端口。 具体检测端口代码: ``` #import <sys/socket.h> #import <arpa/inet.h> #import <fcntl.h> #import <unistd.h> #import <errno.h> BOOL isFridaPortReallyOpen(void) { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) return NO; // 非阻塞 fcntl(sockfd, F_SETFL, O_NONBLOCK); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(27042); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); int ret = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)); if (ret < 0 && errno != EINPROGRESS) { close(sockfd); return NO; } fd_set wfds; FD_ZERO(&wfds); FD_SET(sockfd, &wfds); struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 300 * 1000; // 300ms ret = select(sockfd + 1, NULL, &wfds, NULL, &tv); if (ret <= 0) { close(sockfd); return NO; } // 关键:检查 SO_ERROR int so_error = 0; socklen_t len = sizeof(so_error); getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_error, &len); close(sockfd); return so_error == 0; } ``` 调用方式 ``` if (isFridaPortReallyOpen()) { NSLog(@"Frida 默认端口可访问"); // 触发风控 / 上报 } ``` 基于 Frida 默认端口号(27042)的检测并非绝对可靠。一方面,理论上该端口可能被其他进程占用,从而在极少数情况下产生误报;另一方面,Frida 本身支持通过参数或二次编译的方式修改默认监听端口,一旦攻击者对 Frida Server 进行了“魔改”或端口重定向,该检测方式就可能被直接绕过。 因此,端口号检测更适合作为低成本、快速命中的辅助判断条件,而不应作为唯一的判定依据。将其与虚拟内存特征检测等更底层、更难完全抹除的手段进行组合使用,才能在实际对抗中获得更稳定、可信的检测效果。 ## 2、检测app虚拟内存特征 当 Frida 附加到 iOS App 时,Frida 的代码、数据、JS runtime、字符串全部被加载到“目标 App 进程的虚拟内存空间”中。 Frida 的 JavaScript Runtime 会把“脚本字符串”放入内存,例如: require("frida-objc-bridge") frida/runtime/core.js Frida._loadObjC(); 这些字符串来源于: • 内置 JS 脚本(core.js) • bridge 初始化代码 • RPC 协议字符串(frida:rpc) JS 引擎必须把字符串展开为明文才能执行,所以这些字符串一定存在于堆或只读区。 (1) Frida attach 的真实技术路径 以最典型的 frida-server attach为例: 1. frida-server(系统进程): * 通过**task\_for\_pid()** 拿到目标 App 的 task port 2. 在目标 App 中: * 创建远程线程(**thread\_create\_running**) * 调用**dlopen()** / Mach-O loader 3. Frida Agent 被加载: * 成为目标 App 的一个 dylib * 由 dyld 映射进App 的 VM (2)app被frida附加后的特征关键词 | 序号 | 关键词 | 用途 | | --- | --- | --- | | 1 | frida_gadget | 这是 Frida 在 native 层的“模块身份标识” | | 2 | /frida-core/lib/gadget/gadget.vala | 错误定位,日志,断言信息 | | 3 | frida/runtime/core.js | 定义:Module / Memory / Interceptor、RPC、调度器 | | 4 | frida_agent_main | Agent 的 native 入口,相当于 main() / init | | 5 | frida_dylib_range= | 这是 Frida 内部“自我感知内存边界”的关键字符串 | | 6 | /frida-core/lib/agent/agent.vala | 错误定位,日志,断言信息 | | 7 | frida:rpc | Agent <->Client 的通信协议前缀,JS / Native 双向调用标识 | | 8 | Frida._loadObjC(); | 注册 ObjC / Java runtime,安装 hook handler | | 9 | require("frida-objc-bridge"); | Java / ObjC 方法解析,class 枚举,selector hook | | 10 | /frida-core/lib/payload/spawn-monitor.vala | 错误定位,日志,断言信息 | | 11 | FridaAgentRunner | 负责启动 JS runtime,管理 lifecycle | | 12 | FridaSpawnHandler | 处理 spawn / attach 事件,与 server 协同 | | 13 | FridaPortalClient | Agent RPC / IPC 客户端,与 frida-server 通信 | | 14 | FridaAgentController | Agent 总控,管理模块、脚本、session | | 15 | frida.Error. | JS Error 类型,Native → JS 异常封装 | (3)内存检测frida特征 检测函数 `checkAppMemoryForFrida` 的工作流程: 1) 初始化 - 获取当前进程 task,自地址 0 开始遍历虚拟内存区域。 - 调用 `init_self_image_range` 记录自身镜像范围,后续跳过本进程主二进制区域。 2) 遍历内存区域 - 使用 `vm_region_64` 逐段获取区域起始地址、大小和保护属性。 - 如果区域属于自身镜像则跳过;非 `VM_PROT_READ` 区域也跳过。 3) 读取并扫描 - 为区域大小分配缓冲区,`vm_read_overwrite` 读取该区域内容。 - 遍历明文特征数组 `kFridaPlainList`(包含 Frida 相关字符串,如 `frida_gadget`、`frida/runtime/core.js`、`FridaAgentRunner` 等),对每个特征调用 `memory_contains` 在该缓冲区内查找。 4) 命中处理与日志 - 如命中,`hitCount++`,并使用 `dladdr` 获取当前区域地址对应的模块路径,提取文件名,记录日志: - 内存检测到的特征 - 地址(十六进制) - 模块名 - hitCount 5) 后续与回调 - 释放缓冲区,继续下一个区域;若 `vm_region_64` 失败则结束循环。 - 扫描结束:若 `hitCount > 0` 且传入了 `onDetected`,则执行回调;最后返回 `hitCount`。 ```#include #include <string.h> #include <stdio.h> #include <mach/mach.h> #include <mach-o/dyld.h> #include <dlfcn.h> #include <mach-o/dyld.h> #include <mach-o/loader.h> #include <sys/sysctl.h> #include <unistd.h> #include <stdbool.h> static void *self_image_start = NULL; static void *self_image_end = NULL; // 根据内存地址查找所属的模块名称(使用 dladdr) static const char *find_module_for_address(vm_address_t addr) { Dl_info info; if (dladdr((void *)addr, &info)) { if (info.dli_fname) { return info.dli_fname; } } return "unknown"; } // 日志文件相关 static NSString *ProtecToolLogFilePath(void) { static NSString *logPath = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docDir = paths.firstObject ?: NSTemporaryDirectory(); logPath = [docDir stringByAppendingPathComponent:@"protec_log.txt"]; }); return logPath; } static void ProtecToolWriteLog(NSString *message) { if (message.length == 0) return; NSString *line = [message stringByAppendingString:@"\n"]; NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding]; if (!data) return; NSString *path = ProtecToolLogFilePath(); NSFileManager *fm = [NSFileManager defaultManager]; if (![fm fileExistsAtPath:path]) { [data writeToFile:path atomically:YES]; } else { NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path]; if (!fh) return; @try { [fh seekToEndOfFile]; [fh writeData:data]; } @catch (__unused NSException *e) { } @finally { [fh closeFile]; } } } void ProtecToolClearLogFile(void) { NSString *path = ProtecToolLogFilePath(); if (!path) return; NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:path]) { [fm removeItemAtPath:path error:nil]; } } NSString *ProtecToolReadLog(void) { NSString *path = ProtecToolLogFilePath(); if (!path) return @""; NSError *error = nil; NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; if (!content || error) { return @""; } return content; } #define PTLog(fmt, ...) do { \ NSString *msg__ = [NSString stringWithFormat:(fmt), ##__VA_ARGS__]; \ NSLog(@"%@", msg__); \ ProtecToolWriteLog(msg__); \ } while(0) static void init_self_image_range(void) { if (self_image_start) return; Dl_info info; if (!dladdr((void *)&init_self_image_range, &info)) return; self_image_start = info.dli_fbase; for (uint32_t i = 0; i < _dyld_image_count(); i++) { if (_dyld_get_image_header(i) == info.dli_fbase) { const struct mach_header_64 *mh = (const struct mach_header_64 *)_dyld_get_image_header(i); intptr_t slide = _dyld_get_image_vmaddr_slide(i); uintptr_t maxEnd = 0; const struct load_command *cmd = (const struct load_command *)((uintptr_t)mh + sizeof(*mh)); for (uint32_t j = 0; j < mh->ncmds; j++) { if (cmd->cmd == LC_SEGMENT_64) { const struct segment_command_64 *seg = (const struct segment_command_64 *)cmd; uintptr_t end = seg->vmaddr + seg->vmsize; if (end > maxEnd) maxEnd = end; } cmd = (const struct load_command *)((uintptr_t)cmd + cmd->cmdsize); } self_image_end = (void *)(maxEnd + slide); break; } } } static const char *kFridaPlainList[] = { "frida_gadget", "/frida-core/lib/gadget/gadget.vala", "frida/runtime/core.js", "frida_agent_main", "frida_dylib_range=", "/frida-core/lib/agent/agent.vala", "frida:rpc", "Frida._loadObjC();", "require(\"frida-objc-bridge\");", "/frida-core/lib/payload/spawn-monitor.vala", "FridaAgentRunner", "FridaSpawnHandler", "FridaPortalClient", "FridaAgentController", "frida.Error." }; // 内存扫描工具函数 static int memory_contains(const void *haystack, size_t haystack_len, const void *needle, size_t needle_len) { if (needle_len == 0 || haystack_len < needle_len) return 0; const unsigned char *h = haystack; const unsigned char *n = needle; for (size_t i = 0; i <= haystack_len - needle_len; i++) { if (memcmp(h + i, n, needle_len) == 0) return 1; } return 0; } /* 获取当前进程 task,初始化从地址 0 开始。 用 vm_region_64 逐段枚举虚拟内存区域;失败即结束循环。 只处理具有 VM_PROT_READ 权限的区域。 为区域大小分配缓冲区,用 vm_read_overwrite 把该区域数据读入。 逐个匹配 kFridaPlainList 中的明文关键字;命中则累加 hitCount。 完成后继续下一个区域;循环结束时,如果命中且传入了 onDetected 回调,就执行它。 返回总命中次数。 */ int checkAppMemoryForFrida(void (*onDetected)(void)) { task_t task = mach_task_self(); vm_address_t addr = 0; vm_size_t size = 0; int hitCount = 0; init_self_image_range(); PTLog(@"内存检测--SELF IMAGE RANGE: %p - %p", self_image_start, self_image_end); while (1) { vm_region_basic_info_data_64_t info; mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; mach_port_t object; kern_return_t kr = vm_region_64( task, &addr, &size, VM_REGION_BASIC_INFO_64, (vm_region_info_t)&info, &count, &object ); if (kr != KERN_SUCCESS) break; if ((void *)addr >= self_image_start && (void *)addr < self_image_end) { addr += size; PTLog(@"内存检测--跳过app自身"); continue; } // 只扫描可读内存 if (info.protection & VM_PROT_READ) { void *buffer = malloc(size); if (buffer) { vm_size_t outSize = 0; if (vm_read_overwrite(task, addr, size, (vm_address_t)buffer, &outSize) == KERN_SUCCESS) { for (size_t i = 0; i < sizeof(kFridaPlainList)/sizeof(kFridaPlainList[0]); i++) { const char *needle = kFridaPlainList[i]; size_t needleLen = strlen(needle); if (needleLen == 0) continue; if (memory_contains(buffer, outSize, needle, needleLen)) { hitCount++; // 查找该内存地址所属的模块 const char *moduleName = find_module_for_address(addr); // 提取模块名称(只显示文件名,不显示完整路径) const char *moduleFileName = strrchr(moduleName, '/'); if (moduleFileName) { moduleFileName++; // 跳过 '/' } else { moduleFileName = moduleName; } PTLog(@"内存检测--发现(%s), 地址:0x%llx, 模块:%s, hitCount=%d", needle, (unsigned long long)addr, moduleFileName, hitCount); } } } free(buffer); } } addr += size; } if (hitCount > 0 && onDetected) { PTLog(@"内存检测--执行--onDetected"); onDetected(); } PTLog(@"内存检测--返回--%d", hitCount); return hitCount; } ``` 当app被frida附加,检测frida特征输出信息: ``` 内存检测--发现(FridaAgentRunner), 地址:0x100c1c000, 模块:unknown, hitCount=1 内存检测--发现(FridaSpawnHandler), 地址:0x100c1c000, 模块:unknown, hitCount=2 内存检测--发现(FridaPortalClient), 地址:0x100c1c000, 模块:unknown, hitCount=3 内存检测--发现(FridaAgentController), 地址:0x100c1c000, 模块:unknown, hitCount=4 内存检测--发现(frida.Error.), 地址:0x100c1c000, 模块:unknown, hitCount=5 内存检测--发现(frida.Error.), 地址:0x100c3c000, 模块:unknown, hitCount=6 内存检测--发现(frida_agent_main), 地址:0x10156c000, 模块:unknown, hitCount=7 内存检测--发现(frida_dylib_range=), 地址:0x10157c000, 模块:unknown, hitCount=8 内存检测--发现(require("frida-java-bridge")), 地址:0x101580000, 模块:unknown, hitCount=9 内存检测--发现(frida/runtime/core.js), 地址:0x101580000, 模块:unknown, hitCount=10 内存检测--发现(frida_dylib_range=), 地址:0x101580000, 模块:unknown, hitCount=11 内存检测--发现(/frida-core/lib/agent/agent.vala), 地址:0x101580000, 模块:unknown, hitCount=12 内存检测--发现(require("frida-objc-bridge");), 地址:0x101580000, 模块:unknown, hitCount=13内存检测--发现(FridaAgentRunner), 地址:0x101580000, 模块:unknown, hitCount=14 内存检测--发现(FridaSpawnHandler), 地址:0x101580000, 模块:unknown, hitCount=15 内存检测--发现(FridaPortalClient), 地址:0x101580000, 模块:unknown, hitCount=16 内存检测--发现(FridaAgentController), 地址:0x101580000, 模块:unknown, hitCount=17 内存检测--发现(frida.Error.), 地址:0x101580000, 模块:unknown, hitCount=18 内存检测--发现(frida_agent_main), 地址:0x102804000, 模块:unknown, hitCount=19 ``` ## 3、安全退出App并防止追踪 当检测到frida特征后,执行退出逻辑,并防止检测代码位置暴露,更好的保护检测逻辑,防止绕过。我们的退出代码需要切断「检测 → 退出」之间的可还原因果关系,让逆向者无法通过崩溃、堆栈、日志反推出检测代码。下面的代码可以很好的实现我们的需求。 ``` void trigger_crash_async(void) { void *p = malloc(16); free(p); dispatch_async(dispatch_get_main_queue(), ^{ CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{ volatile char *q = (char *)p; q[0] = 0x42; }); }); } ``` 通过 Use-after-free + 异步调度 + RunLoop 执行,在时间、线程、调用栈和语义四个层面切断了“决策 → 崩溃”的因果关系,使崩溃在日志和运行期分析中高度拟态为真实工程 Bug,从而显著提高反追踪与反定位成本。 调用堆栈无法直接找到检测代码和崩溃位置: ``` Termination Signal: Bus error: 10 Termination Reason: Namespace SIGNAL, Code 0xa Terminating Process: exc handler [9303] Triggered by Thread: 0 Thread 0 name: Dispatch queue: com.apple.main-thread Thread 0 Crashed: 0 libobjc.A.dylib 0x00000001a82c4c98 objc_retain + 8 1 CoreFoundation 0x0000000194abbf14 __NSSingleObjectArrayI_new + 84 2 CoreFoundation 0x000000019496b334 -[NSArray initWithArray:range:copyItems:] + 412 3 UIKitCore 0x00000001972cf714 _runAfterCACommitDeferredBlocks + 160 4 UIKitCore 0x00000001972beb4c _cleanUpAfterCAFlushAndRunDeferredBlocks + 200 5 UIKitCore 0x00000001972f0260 _afterCACommitHandler + 76 6 CoreFoundation 0x00000001949ffecc __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32 7 CoreFoundation 0x00000001949fa5b0 __CFRunLoopDoObservers + 604 8 CoreFoundation 0x00000001949faaf8 __CFRunLoopRun + 960 9 CoreFoundation 0x00000001949fa200 CFRunLoopRunSpecific + 572 10 GraphicsServices 0x00000001aaaf5598 GSEventRunModal + 160 11 UIKitCore 0x00000001972c0004 -[UIApplication _run] + 1052 12 UIKitCore 0x00000001972c55d8 UIApplicationMain + 164 13 TestSpace 0x00000001046deb6c main + 184 14 libdyld.dylib 0x00000001946d9598 start + 4 ``` ## 总结 任何单一检测手段都不可能对 Frida 实现“绝对防御”。在真实对抗场景中,Frida 的行为和特征本身也可以被刻意隐藏或篡改。因此,更合理的策略是将端口检测与内存特征检测进行组合使用,并配合多时机、多路径的触发机制,从而显著提高攻击成本和逆向复杂度。 Frida 防御的核心目标,是在可控的性能与稳定性成本下,尽早发现异常环境、干扰分析过程、并保护关键业务逻辑不被轻易理解和复现。希望本文的思路与实现方式,能为 iOS 开发与安全人员在实际项目中构建 Frida 对抗能力提供有价值的参考。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#逆向分析
收藏
・
7
点赞
・
10
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
谢谢你的细致分析,受益匪浅!
2026-7-16 09:49
kisanakzz
感谢你分享这么好的资源!
2026-4-13 00:20
fei3ei
非常支持你的观点!
2026-1-27 10:06
Yangser
期待更多优质内容的分享,论坛有你更精彩!
2026-1-8 20:20
东方玻璃
感谢你的贡献,论坛因你而更加精彩!
2026-1-1 22:19
mb_kpxkzofj
感谢你的积极参与,期待更多精彩内容!
2025-12-31 14:43
xiaoyiny
为你点赞!
2025-12-31 14:17
luoyanbei
+10
期待更多优质内容的分享,论坛有你更精彩!
2025-12-31 14:14
Shangwendada
感谢你分享这么好的资源!
2025-12-31 14:10
mb_cfjwplfo
感谢你分享这么好的资源!
2025-12-31 13:56
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
Yangser
雪 币:
1953
活跃值:
(4173)
能力值:
( LV4,RANK:50 )
在线值:
发帖
11
回帖
129
粉丝
217
关注
私信
Yangser
2
楼
我去怪不得我iOS上的frida被检测了,退出之后的.ips日志里显示的崩溃堆栈都是系统库相关的,,,像这种利用UAF的退出机制,应该如何定位frida检测点呢?
2026-1-13 14:10
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
三六零SRC
42
发帖
39
回帖
30
RANK
关注
私信
他的文章
[原创]iOS安全开发中的Frida检测
3535
[讨论]利润仅$200?Anthropic 最新研究揭示 AI 自动挖掘 0-day 的真实经济账
2572
[原创]浅谈SQL注入手工测试思路
3889
VMP的手动分析和AI还原
4927
[原创]Python代码保护之重置操作码映射的攻与防探究(一)
2032
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部