iOS逆向分析:拆解抢红包与防撤回插件样本
简介面向iOS微信的自动抢红包与防撤回插件包专供越狱环境下有插件使用、调试或学习逆向需求的开发者。资源完整收录插件工程与编译产物共30个文件涵盖5个不同版本的安装包、动态库文件、属性列表配置、工程文件以及8张操作截图整包仅817KB紧凑易用。已有520人学习下载包内提供阅读说明与多版本安装包可依据需要选用或对比新旧差异。目录按源码、截图、安装包与配置分层放置并附有说明文档便于按模块查阅。源码部分包含实现文件与工程配置可查看自动抢红包的触发逻辑、消息撤回的拦截方式并通过属性列表自定义参数另附控制文件与打包清单等辅助内容对于理解通知回调、插件注入体系以及越狱插件分发结构的读者具有直接的参考价值。1. 拿到「抢红包防撤回」样包先别急着上真机拆开看最近有群里丢出一份zip写着iOS自动抢红包和防撤回插件附带一句“越狱手机装上就能用”。好些人第一反应是问能不能装到自己手机上。我先把它拖进隔离目录拆了一遍——这类插件不是简单小工具而是针对某即时通讯类App的完整注入攻击链重打包、动态库注入、Method Swizzling、越狱检测绕过全占了。装上去轻则闪退白苹果重则账号被风控拉黑。这篇文章顺着这份样本的拆解走一遍先看压缩包内部结构再拆防撤回和抢红包背后的Hook机制接着把复现时踩过的坑原样摆出来最后落到防御端给App开发者和测试工程师一套自查脚本。适合三拨人搞iOS逆向的、维护即时通讯类App的、以及天天在越狱环境里怕翻车的测试。2. 样本解剖从压缩包到注入态理清三层结构2.1 先跑unzip -l看清单别双击先看内容拿到任何未知zip我的习惯是先列清单再解压尤其这种声称“插件”的包。复制到隔离目录后执行unzip -l iOS自动抢红包和防撤回插件.zip输出大致是这个结构文件路径与时间戳已脱敏Archive: iOS自动抢红包和防撤回插件.zip Length Date Time Name --------- ---------- ----- ---- 4096 2024-xx-xx xx:xx Payload/ 201326592 2024-xx-xx xx:xx Payload/某IM工具.app/某IM工具 94208 2024-xx-xx xx:xx Payload/某IM工具.app/Inject.dylib 3180 2024-xx-xx xx:xx Payload/某IM工具.app/Info.plist 988 2024-xx-xx xx:xx Payload/某IM工具.app/embedded.mobileprovision 3850 2024-xx-xx xx:xx README.txt注意三点。第一这份zip里有完整的Payload目录说明它是已经重打包好的.app不是插件源码工程装的时候整个文件夹拖进设备就行普通用户根本不知道里面多了个Inject.dylib。第二主二进制接近200MB说明它是一份从官方渠道抓下来、脱过壳的完整App拷贝不是空壳项目。第三README.txt大概率是作者写给使用者的安装说明读它能快速知道作者暴露了哪些预期功能比如“抢红包延迟XX毫秒”“防撤回仅支持文本”这些数字后面分析时都用得上。解压的时候别用访达双击命令行解压更可控mkdir sample_extract cd sample_extract unzip ../iOS自动抢红包和防撤回插件.zip解压完先看README。我一般会把里面提到的延迟参数、功能列表和实际代码里的实现对照一遍。很多插件作者README写的和代码实际做的不一样——有人为了快速出包README说支持某版本代码里却是另一套hook点这种不一致本身就是很好的分析线索。2.2 file、otool、nm三连命令完成第一轮定位接下来对主二进制做第一轮静态定位。三个命令按顺序跑依次回答“这是什么架构的包”“被注入了什么库”“插件的基础设施长什么样”。# 1. 架构与平台 file Payload/某IM工具.app/某IM工具 # 2. 动态库依赖 otool -L Payload/某IM工具.app/某IM工具 # 3. 导出符号里的特征类名 nm -gU Payload/某IM工具.app/某IM工具 | grep -iE hook|tweak|revoke|packet|card | head -30第一个命令的输出一般是Mach-O 64-bit executable arm64或arm64e。arm64e对应A12及以上芯片很多老插件没适配光是这一步就能判断装上后会不会在特定机型闪退。第二个命令是关键otool -L会列出主二进制链接的所有动态库正常情况下只有系统库和App自身的framework如果列表里出现了Inject.dylib或名字里带tweak的库说明这个包已经是注入后的成品。第三个命令把插件作者留下的类名和符号名全部暴露出来比如看到WReq_TweakHook、AASwizzledRecord这种前缀就能反推出hook点大概在哪个模块。把命令结果分成三类整理无害的系统库引用、可疑的第三方dylib、命名带攻击意图的导出符号。这一步不需要逆向能力纯靠眼力加经验却能把一份包的信任等级从“看起来能用”降到“必须隔离分析”。顺带说一句如果你在nm结果里看到_substrate开头的符号说明这玩意儿是走Substitute/Substrate体系的老tweak那它对后续iOS版本的兼容性大概率是玄学复现时别抱太高期望。2.3 重打包的三道坎脱壳、重签名、完整性校验分析这种压缩包有个背景得先说明白。正常从App Store下载的App二进制是加密的代码段直接dump出来没法看更别说改完重装。所以制作这类插件包的人第一件事就是脱壳——把加密的二进制在内存中dump出来再保存。常见操作是用脱壳工具在越狱设备上把运行中的进程内存抠出来得到一份“裸奔”的Mach-O。这份裸奔二进制就是我们在2.1看到的近200MB主文件。拿到裸奔二进制之后第二道坎是重签名。iOS只运行带合法签名的App插件作者需要用自己的证书重新签整个.app再把插件dylib加进去。分析时如果想把这份样本跑起来常见做法是# 给可执行文件补get-task-allow允许调试器附加 ldid -S -M Payload/某IM工具.app/某IM工具 # 用自签证书对整个app重签 codesign -f -s Apple Development: 自签名证书名称 Payload/某IM工具.app --deepldid -S -M里的-S是写入签名-M是补上get-task-allow权限否则后续调试器没法attachcodesign -f -s是强制用指定证书重签--deep会递归签掉包内所有嵌套文件。这一步过不了设备直接拒绝安装。第三道坎是完整性校验。目标App自己往往带越狱检测和签名校验插件作者为了骗过它要么在注入代码里hook掉检测函数要么用“检测什么就patch什么”的思路在启动早期替换返回值。分析时最怕碰到既有签名校验又有越狱检测的版本两者叠加会让复现过程变得非常不可控。我的经验是先把护栏拆掉跑通一次再一项一项加回来看哪一步开始触发闪退这样能准确定位校验点在哪里。3. 运行时Hook机制拆解防撤回和抢红包的底层原理3.1 防撤回的最短依赖路径Method Swizzling拦截删除方法先看防撤回。某IM工具里撤回消息的完整链路大致是用户在A端发起撤回服务端通知B端“这条消息要删除”B端的消息管理器收到通知后调用数据库删除接口再通知UI层刷新聊天列表。对插件来说动手脚的位置有两个——数据层删除之前或者UI层刷新之前。常见做法是Method Swizzling把消息管理器里负责删除的那个实例方法和插件自己写的方法做交换。下面是一段用于定位思路的伪代码不是完整插件代码// 伪代码描述防撤回hook点分析思路 #import objc/runtime.h static void (*orig_deleteMessage)(id self, SEL _cmd, id messageId, void (^completion)(BOOL)); static void custom_deleteMessage(id self, SEL _cmd, id messageId, void (^completion)(BOOL)) { // 1. 先把即将删除的消息体复制一份存到插件自己的容器 // 2. 调用原实现让上层以为删除已经完成 // 3. 在UI层注册观察者把保存的消息重新插回cell orig_deleteMessage(self, _cmd, messageId, completion); } void installRecallHook(void) { Class cls NSClassFromString(YYMessageManager); // 目标类名脱敏 SEL sel NSSelectorFromString(deleteMessageWithId:completion:); Method m class_getInstanceMethod(cls, sel); Method myM class_getInstanceMethod([self class], selector(custom_deleteMessageWithId:completion:)); method_exchangeImplementations(m, myM); }注意这段代码里class_getInstanceMethod和method_exchangeImplementations是核心前者拿到目标类的原始Method结构后者原地交换IMP指针。真正写的时候还有一个细节orig_deleteMessage不能直接等于method_getImplementation(m)因为交换之后再去调旧实现会递归必须先保存好原IMP再交换。我见过不少新手在这一点上翻车现象就是插件一装上对方撤回消息时App直接崩溃。从分析角度看到代码里出现NSClassFromString加一段字符串拼出来的类名基本就能判断作者是在运行时动态取类而不是编译期写死。这种写法一方面是绕过静态扫描另一方面也说明他对目标App版本的依赖很强——目标App一换内部类名插件立刻失效。这是这类插件的通病也是一线运维最喜欢的防御思路频繁改动内部类名就能让旧版本插件集体罢工。3.2 抢红包的触发链消息到达、卡片类型判断与异步队列抢红包比防撤回复杂一个量级因为它涉及UI操作。插件要盯住的目标消息到达事件判断消息是不是红包卡片然后触发点击。整个触发链分四步。第一步注册消息到达监听。作者通常在消息管理器的incomingMessage:回调处做Swizzling或者hook数据库插入前的代理方法。第二步判断卡片类型。红包消息在数据模型里一定有一条标识字段不同版本字段名不同一般是sType或bizType。插件拿到模型后用KVC读字段匹配到红包类型才继续。第三步计算入口位置。这一步最容易出问题红包消息可能在聊天列表、可能在会话页、还有可能已经被折叠插件需要根据当前UI状态决定是直接触发红包页面还是先滑动列表。第四步把点击动作抛到主线程。iOS的UI操作必须在主线程而消息到达回调可能不在主线程所以常见的做法是dispatch_async(dispatch_get_main_queue(), ...)再执行点击。下面是一段简化的Frida脚本用来验证第二步“字段判断”是否命中。这类脚本在分析时很有用不需要改任何代码就能观察数据流向// 用Frida观察红包消息的字段读取行为 if (ObjC.available) { const MessageManager ObjC.classes[YYMessageManager]; // 脱敏类名 if (MessageManager) { const handler MessageManager[- incomingMessage:]; if (handler) { handler.implementation function (self, selector, msg) { // 调用原实现先保证App行为正常 const ret handler.apply(self, arguments); try { const bizType msg.valueForKey_(bizType); const sType msg.valueForKey_(sType); if (String(bizType) 1002 || String(sType) redpacket) { console.log([*] 命中红包卡片写入日志); console.log([*] 消息体摘要: msg); } } catch (e) { console.log([-] 字段读取异常: e); } return ret; }; } } }这段脚本的价值在于原实现照常执行只在旁边打日志你不需要猜代码里写死了哪个字段跑一遍真实消息就能看到命中日志。valueForKey_对应Objective-C的KVC是读取动态字段的最快方式String(bizType)强转字符串是因为回调里的值可能是NSNumber直接拼接会崩。实战中抢红包插件真正翻车的地方通常在第四步异步队列处理的是快照原始消息体在下一个runloop被容器释放等队列轮到它执行时只剩一个空壳。后面第四章节会专门写这个坑。3.3 加载顺序陷阱同一个插件内的Swizzle时序分析样本时我还注意到一个很多人忽略的点插件自己内部的方法交换是有顺序依赖的。比如防撤回模块要先hook消息管理器的删除方法抢红包模块再hook同一个管理器的消息到达方法如果两个模块都放在load里load的执行顺序取决于dylib里目标文件的链接顺序而不是你肉眼看到的代码顺序。一旦顺序反了典型副作用是抢红包模块先执行hook掉了消息到达方法随后防撤回模块要的原始删除方法指针已经被换过一次两个模块各自保存的orig_指针指向了同一块位置逻辑彻底错乱。表现成症状就是装完插件后App能启动但撤回的消息偶尔消失、红包偶尔漏抢重启一次又好一阵纯玄学问题。排查顺序问题我用一个笨但有效的办法在两个模块的构造入口分别加NSLog打印时间戳放进真机跑一段看日志顺序。或者用image list -o -f在LLDB里看dylib的加载地址顺序这个顺序其实就是load的执行顺序# LLDB中查看插件相关image的加载顺序 image list -o -f | grep -E Inject|Substrate|某IM工具.app输出里从上到下就是加载先后。如果发现Inject.dylib里的两个子模块顺序不对就得回到源码里在链接阶段调整顺序或者在启动早期用单例统一调度别让模块各自为战。这些都是分析样本时推理出来的结论不是作者写的文档里能看到的但恰恰是这类项目最容易出问题的地方。4. 调试复现中的避坑与排查四条实测记录4.1 坑一越狱检测把插件按在地上摩擦现象按样本包重签名装到越狱测试机上App启动后1到2秒立刻闪退连主界面都见不到。系统日志里能看到Jailbreak detected之类的字符串但普通模式下没有任何弹窗提示。原因目标App内置了越狱检测检测项包括但不限于常见越狱工具的文件路径是否存在比如Cydia.app的路径、当前进程是否链接了Substrate相关库、是否能在沙盒外写文件。插件作者在样本里做了一部分绕过但漏掉了其中一个检测点——新版目标App把检测逻辑放到了启动早期的C构造函数里比插件自己的Objective-Cload更早执行所以插件还没来得及做任何绕过动作就已经被判了死刑。解决先用Frida在启动早期拦截检测函数。具体做法是把frida-server跑在设备上写一个脚本hook掉NSFileManager的fileExistsAtPath:把所有越狱路径的查询全部返回NO并hook掉dlopen调用记录看App启动时都加载了什么库。定位到检测点之后在插件的load最前面再挂一层Swizzle保证先于App自身的构造函数执行。用我们团队的话说这叫“在裁判吹哨之前先捂住裁判的眼睛”。4.2 坑二注入后秒退问题出在符号查找空指针现象同样的插件包在A机型上跑得好好的换到B机型装完启动必崩崩溃日志指向objc_msgSend没有明显业务错误堆栈。原因不是机型问题是系统版本差异导致内部类名和实例方法名对不上。插件代码里写死的NSClassFromString(YYMessageManager)在A版本的App里存在在B版本里类已经改名成了YYIMNewMessageManager于是class_getInstanceMethod返回NULL后面直接method_exchangeImplementations把NULL当参数传整个运行时直接挂在消息发送环节。解决这是百分之百的防御性编程问题。所有hook前的查找步骤都要判空Class cls NSClassFromString(YYMessageManager); if (!cls) { cls NSClassFromString(YYIMNewMessageManager); } if (!cls) { NSLog([-] 未找到目标类跳过hook); return; } Method m class_getInstanceMethod(cls, NSSelectorFromString(deleteMessageWithId:completion:)); if (!m) { NSLog([-] 未找到目标方法跳过hook); return; }这段代码的价值不只是防御崩溃还能在分析阶段帮助你快速判断版本兼容范围。你在逆向一份未知样本时把这种判空逻辑加进去跑一遍输出日志能直接得出“这个插件最低兼容到哪个App版本”的结论。很多样本作者自己没写判空恰恰说明他们只在自己那一两个版本上测试过这类样本的信任等级又要再降一等。4.3 坑三抢红包只抢第一个异步队列丢任务现象复现时拉一个测试群连续发10个红包插件只抢到第1个后面9个完全没有反应。杀掉App重开又只抢第1个。群里有人说话消息能正常收到说明Hook本身没失效。原因插件作者用了一个全局单例队列串行处理红包卡片每条消息到达时先放进队列后台线程挨个处理。问题出在红包消息在数据库写入完成之前就被队列消费了。也就是说插件从消息到达回调里取到的消息模型在进入队列时还只是一个没有完全初始化的临时对象等到后台线程真正执行点击逻辑时临时对象已经被释放点击坐标计算出来全是无效值。之所以每次都先抢到第1个是因为第一条消息的时序恰好赶上了数据库写入完成后面的全部翻车。解决在队列里加延迟重试等数据落地后再处理。我改造样本时做的事是把“从模型取卡片类型”这个动作从入队阶段挪到出队阶段执行出队时先检查模型是否可用不可用就dispatch_after200毫秒再放回去重试。为什么是200毫秒这是我在测试设备上连续跑50组消息统计出来的经验值低于150毫秒重试成功率骤降高于500毫秒已经能明显感觉到抢红包变慢容易被用户察觉。这种参数属于典型的经验参数换个设备和App版本就得重新测。顺带说一句分析这类异步链路最有效的工具不是日志而是断点加thread backtrace。在出队处理函数上打断点每次命中都看一眼当前线程和对象内存地址能非常直观地看出对象是否已经被释放。4.4 坑四防撤回失效viewModel与view的Hook位置摆错了现象防撤回模块装上后当时对方撤回的消息确实还在屏幕上但退出聊天界面再进来那条消息还是消失了。更糟的是有时候撤回一条消息界面上出现两条一模一样的下拉刷新后消失。原因防撤回只做对了一半。作者把Swizzle挂在了消息管理器的删除方法上保存了一条消息副本却没有拦截UI层的数据源刷新。聊天界面的消息列表由viewModel驱动viewModel收到删除通知后把内存里的消息数组remove掉再调tableView刷新。插件保存的是原始数据库消息但viewModel已经把它从数组里删了所以你看到的“还在”不过是UI没刷新前的残影一旦重新进入聊天页viewModel重建列表残影自然没了。而出现两条重复消息是作者为了补救在另一个位置又加了一次插入没做幂等判断。解决hook点必须下沉到viewModel层。正确的位置是拦截viewModel里处理消息删除的那个方法删除动作执行之前把消息体转成JSON存下来然后hook聊天列表的数据源代理方法在返回消息数组时把存下来的消息重新拼进去并且用消息id做去重。这样无论是退出重进还是下拉刷新消息都能稳定留在界面上。分析样本时判断作者到底hook对没有一个快速方法是触发撤回后立刻断点停住打印viewModel里消息数组的count如果count已经减少说明他hook的是UI层而不是数据层这个样本的一部分功能实际上根本没生效。5. 防御视角与自查脚本从样本特征反推到落地排查5.1 四个高价值信号样本特征反查表拆完样本反过来要回答一个问题作为某IM工具这类App的开发者怎么尽早发现自己被类似插件盯上我把这份样本的特征整理成一张表可以直接拿去做人工巡查的检查项特征样本中的表现防御端排查方法动态库注入Inject.dylib被注入主二进制依赖列表可查启动时枚举_dyld_image_count和_dyld_get_image_name比对自身的framework白名单类名动静分裂运行时NSClassFromString拼类名静态扫描无特征代码混淆或频繁改类名前缀让硬编码类名的脚本失效敏感方法交换删除消息、消息到达等方法被Swizzle对关键方法保存原始IMP在调用链里校验IMP地址是否变化越狱环境依赖通过越狱检测绕过逻辑确认环境不主动触发闪退上报环境特征到服务端做风控这张表背后有一个更通用的问题攻击者永远在重新打包你这个App。所以防御的第一优先级不是让插件装不上而是让插件装上但干不了活——类名不固定、关键方法IMP做校验、关键路径上报服务端这三板斧下来插件作者维护成本指数级上升。样本里已经暴露了两个弱点一是硬编码类名二是只看本地UI这两点恰恰是风控端最容易做文章的。5.2 启动时完整性自检一段可以塞进App的检查代码针对动态库注入和SwizzleApp侧最直接的防御是启动时自检。我把样本分析得到的检测思路整理成一段可落地的Objective-C代码在启动早期调用#import mach-o/dyld.h // App启动早期调用做完整性检查 - (void)integrityCheck { // 1. 枚举当前进程加载的所有image检查白名单 uint32_t count _dyld_image_count(); NSArray *whitelist [/System/Library, /usr/lib, 你的App所在目录]; NSMutableArray *offenders [NSMutableArray array]; for (uint32_t i 0; i count; i) { const char *name _dyld_get_image_name(i); NSString *nsName [NSString stringWithUTF8String:name]; BOOL inWhitelist NO; for (NSString *prefix in whitelist) { if ([nsName hasPrefix:prefix]) { inWhitelist YES; break; } } if (!inWhitelist) { [offenders addObject:nsName]; } } if (offenders.count 0) { // 上报异常image列表到服务端本地不闪退避免被绕过 NSLog([安全] 发现非白名单动态库: %, offenders); } }这段代码的核心是白名单前缀匹配。_dyld_image_count和_dyld_get_image_name是动态加载器的公开API能拿到当前进程全部镜像路径。为什么异常时不闪退而是只上报因为闪退会暴露检测逻辑攻击者可以逐步试探哪一步导致闪退再针对性绕过静默上报让服务端积累证据等风控模型跑完再统一动作。这是样本分析教会我的一个反直觉经验安全功能最大的敌人是每次都被精确测出来。5.3 落成CI脚本一条命令出检测结果服务端上报不适合所有团队但有一件事适合所有人——把检测脚本落到CI配合日常打包流程跑。下面是一个极简的bash脚本对ipa包做静态检查不需要真机就能排查重打包特征#!/bin/bash # 静态检查ipa是否被重打包注入 IPA_PATH$1 if [ -z $IPA_PATH ]; then echo 用法: ./check_repacked.sh YourApp.ipa exit 1 fi TMPDIR$(mktemp -d) unzip -q $IPA_PATH -d $TMPDIR MAIN_BIN$TMPDIR/Payload/$(ls $TMPDIR/Payload | head -1) echo 1. 检查签名信息 codesign -d --entitlements - $MAIN_BIN 2/dev/null | grep -E application-identifier|get-task-allow || echo 签名读取失败 echo 2. 检查非系统动态库 otool -L $MAIN_BIN | grep -v -E ^/System|^/usr/lib|rpath|executable_path | awk {print $1} || echo 未发现异常dylib echo 3. 检查敏感类名 strings $MAIN_BIN | grep -iE tweak|hook|substrate|cycript|frida || echo 未发现敏感字符串脚本做了三件小事签名信息检查、动态库过滤、字符串特征扫描。otool -L的过滤条件里排除系统路径和rpath、executable_path剩下的路径基本就是被硬塞进去的dylibstrings那一步能扫出插件作者来不及清理的特征字符串。这个脚本跑一次只要几秒加进CI的post-job从此每次出包都能拿一份基线报告。它不解决全部问题但至少能让重打包这类低技术门槛攻击在提测前就被拦住。6. 最后一个动作用10分钟跑一个mini Hook验证结论前面几章都是围绕样本的静态和动态分析最后给一个可以立刻动手的验证动作用Frida在测试设备上挂一个mini Hook验证上述分析的结论是否在自己的环境里成立。不需要重新编译任何代码只要设备上有frida-server脚本跑起来就能观察。// verify_hooks.js: 验证目标App的hook点是否存在并可用 if (ObjC.available) { var targets [ { cls: YYMessageManager, method: - deleteMessageWithId:completion: }, { cls: YYMessageManager, method: - incomingMessage: }, { cls: YYChatViewModel, method: - removeMessagesWithIds: } ]; targets.forEach(function (t) { var cls ObjC.classes[t.cls]; if (!cls) { console.log([-] 类不存在: t.cls); } else { var m cls[t.method]; if (m) { console.log([] 方法命中了hook点: t.cls t.method); } else { console.log([-] 方法不存在: t.cls t.method); } } }); }这段脚本跑完你能得到一张hook点可用性清单。对照样本里的硬编码类名如果某个类在你这台设备上返回不存在就不用继续费时间深挖了——这版App已经让插件失效。这也是我建议所有做逆向分析的人保持的习惯先验证hook点再谈深入分析能省掉大量无用功。验证清单建议固定成五个问题每次换新样本都先过一遍验证项判定标准注入dylib是否被加载image list能看到Inject相关的image目标类是否存在Frida脚本返回[]而不是[-]目标方法是否可调用在方法入口breakpoint能命中Swizzle后原逻辑是否保留原始功能没被破坏撤回和消息收发正常重签名后App能稳定运行5分钟不闪退日志无异常这五个问题全过样本才值得继续深挖任何一项不过要么换样本版本要么接受“这包在自己手里跑不起来”的现实别在它身上继续耗时间。对了再说一个我保持了很久的习惯每拆完一个样本我会把分析结论写进团队的安全备忘再把测试设备恢复到干净环境样本包挪到隔离目录存档。如果你手里也有一份同类样本先别急着双击从unzip -l那一步开始走一遍后面就不会被坑到。不是不信任结论而是这类样本太容易污染环境——上一份的残留Hook会干扰下一份的分析结论一次误判就会让后面整整一周的排查方向跑偏。从那以后我每次拆完样本都强制走一遍环境还原流程宁可多花十分钟也不想带着脏环境开工。希望帮到你。本文还有配套的精品资源点击获取