iOS逆向工程实战:某信营业厅App算法分析与签名破解 1. 项目概述与逆向工程的价值最近在移动安全研究圈里一个关于“某信营业厅”App的算法分析项目引起了我的注意。这个标题本身就充满了信息量“iOS 逆向”指明了平台和技术手段“某信营业厅”锁定了目标应用而“算法分析”则是最终目的。这其实是一个典型的移动应用安全研究案例其核心在于通过逆向工程技术去窥探一个商业级App内部的核心业务逻辑——尤其是那些与用户认证、数据加密、接口签名相关的算法。对于从事移动安全、风控对抗、协议分析甚至是自动化脚本开发的同行来说这类工作具有极高的实战价值。它不仅能帮助我们理解一个成熟应用是如何保护其通信和数据安全的更能让我们在面对类似的黑盒系统时拥有一套行之有效的分析方法和工具链。为什么是“某信营业厅”这类运营商官方App通常集成了复杂的业务系统包括话费查询、套餐办理、积分兑换、在线充值等。这些业务背后往往伴随着与服务器进行敏感数据交互的需求因此其通信协议的安全性设计通常会比较完善可能涉及非对称加密、自定义的哈希算法、时间戳签名、设备指纹绑定等多种技术。成功逆向分析其算法就相当于拿到了一把钥匙可以让我们以程序化的方式模拟客户端行为实现自动化查询或进行更深层次的安全审计。当然这一切的前提是必须严格遵守法律法规将技术用于授权测试、安全研究或个人学习的目的。2. 逆向分析前的环境与工具准备工欲善其事必先利其器。iOS平台的逆向分析与安卓平台有显著不同其封闭性更高因此工具链的选择和环境的搭建尤为关键。整个准备工作可以分为越狱环境、分析工具和动态调试配置三个部分。2.1 越狱设备与系统选择进行深入的iOS逆向一台越狱的iPhone或iPad是必不可少的。目前基于常见的实践对于A12及以上芯片的设备checkra1n越狱工具利用硬件漏洞兼容性较好但需要电脑引导。而对于软件漏洞越狱如unc0ver或Taurine则需要关注其支持的iOS版本范围。我的建议是准备一台专门用于测试的旧款设备如iPhone 6s至iPhone X之间的型号将其系统保持在iOS 14.0 - iOS 15.4.1这个相对“黄金”的越狱版本区间。选择“某信营业厅”的一个较旧但功能完整的版本进行分析可以避开高版本系统更强的反调试和代码混淆机制降低入门门槛。在越狱完成后首要任务是通过Cydia或Sileo等包管理器安装必备的底层工具。这包括OpenSSH用于通过电脑远程登录到iOS设备执行命令和传输文件这是所有操作的基础。adv-cmds提供ps、top等进程查看命令方便我们找到目标App的进程信息。Filza File Manager一个强大的文件管理器可以直观地浏览iOS文件系统查看和修改应用沙盒内的数据。2.2 静态分析与动态调试工具链静态分析用于在不运行程序的情况下查看其代码和资源而动态调试则是在程序运行时进行观察和干预。静态分析工具dumpdecrypted/frida-ios-dump用于从越狱设备上砸壳decrypt。App Store下载的应用都是经过苹果加密的DRM直接拖出来的二进制文件是无法分析的。这些工具能在应用运行时从内存中将解密后的可执行文件Mach-O dump 出来。Hopper Disassembler或IDA Pro反汇编神器。将砸壳后的Mach-O文件拖入可以将机器码反汇编成可读的汇编代码ARM64。IDA的高级版本还支持反编译为伪C代码Pseudocode这能极大提升分析效率。对于初学者Hopper的入门成本相对较低。class-dump这是一个命令行工具专门用于从Objective-C运行时信息中提取头文件。对于像“某信营业厅”这类很可能使用OC或OC/Swift混编的应用class-dump可以直接暴露出所有的类名、方法名和属性为我们提供一张清晰的“地图”。动态调试与Hook工具Frida动态插桩框架的王者。通过注入JavaScript脚本可以在应用运行时拦截函数调用、修改参数和返回值、枚举类和方法。它的frida-trace命令能快速追踪某个库的函数调用是定位关键算法的利器。在iOS上使用需要安装FridaServer。Cycript或LLDBCycript是一个混合了Objective-C和JavaScript语法的交互式控制台可以附着到进程上动态执行代码和查询对象信息。LLDB是更底层的调试器功能强大但学习曲线陡峭通常与debugserver配合使用。MonkeyDev这是一个集成的开发环境它基于Xcode和Theos可以方便地创建注入动态库的插件工程对目标App进行Hook和修改并直接安装到设备上测试非常适合进行功能修改或算法验证阶段的开发。2.3 网络抓包环境配置算法分析离不开对网络请求的观察。我们需要捕获App与服务器之间的所有HTTP/HTTPS流量。电脑端代理在电脑上运行Charles或mitmproxy。Charles图形化界面友好mitmproxy命令行功能强大。将其设置为代理服务器如8888端口。设备代理配置在iOS设备的Wi-Fi设置中配置HTTP代理指向电脑的IP地址和端口如192.168.1.100:8888。安装SSL证书为了解密HTTPS流量必须在iOS设备上安装并信任代理工具生成的根证书。在Charles中通过Help - SSL Proxying - Install Charles Root Certificate on a Mobile Device获取安装指引。SSL Pinning绕过很多应用包括“某信营业厅”会使用SSL证书绑定Pinning技术来防止中间人攻击。这时直接抓包会失败。我们需要使用Frida脚本或SSL Kill Switch 2这样的越狱插件来禁用证书验证。一个常用的Frida脚本是拦截NSURLSession和AFNetworking等网络库的证书验证方法。注意配置抓包环境时务必确保电脑防火墙允许代理端口的连接并且手机和电脑处于同一局域网。如果遇到抓不到包的情况首先检查代理IP和端口是否正确其次检查SSL证书是否已安装且被完全信任需要在“设置-通用-关于本机-证书信任设置”中启用。3. 目标应用初探与关键点定位拿到“某信营业厅”的IPA文件可以从越狱设备已安装的目录/var/containers/Bundle/Application/下拷贝或通过第三方商店下载后不要急于深入汇编代码。有条不紊的信息收集能事半功倍。3.1 应用拆解与资源探查首先将.ipa文件后缀改为.zip并解压得到一个Payload文件夹里面就是.app包。我们可以直观地看到Info.plist查看CFBundleExecutable字段确定主二进制文件名称例如ChinaUnicom查看CFBundleIdentifier获取包名。有时这里还会有一些自定义的URL Scheme。资源文件图片、.js、.html、.json配置文件等。特别是.js文件如果应用内嵌了H5页面其部分业务逻辑和加密可能在前端完成。Frameworks文件夹查看应用使用了哪些第三方动态库。例如看到AFNetworking.framework就知道网络层用了这个库看到libcrypto.a可能使用了OpenSSL进行加密。接下来使用otool -L 二进制文件路径命令查看该二进制文件链接了哪些系统库和自定义库。重点关注Security.framework加解密、CommonCrypto系统加密库、JavaScriptCore.frameworkJS引擎等。3.2 Class-Dump获取类信息这是定位代码入口的关键一步。将砸壳后的主二进制文件例如ChinaUnicom拷贝到电脑使用class-dump导出头文件class-dump -H ChinaUnicom -o ./headers/执行后会在./headers/目录下生成成百上千个.h文件。这时我们需要用一些技巧来缩小范围关键词搜索在导出的头文件中搜索与“算法”、“加密”、“签名”、“网络”相关的关键词。例如grep -r “sign\|Sign\|encrypt\|Encrypt\|auth\|Auth\|token\|Token\|security\|Security” ./headers/ --include*.h常见的类名可能包含SecurityManager、EncryptTool、NetworkSign、APIManager等。分析网络请求类寻找类似XXXRequest、XXXAPI、XXXNetworkClient的类。查看它们的方法特别是那些参数中包含NSDictionary *params返回值是NSURLSessionTask *或带有success/failureblock的方法。这些往往是发起网络请求的入口。查看工具类寻找XXXUtility、XXXHelper、XXXCommon这样的工具类其中常常封装了哈希、AES、RSA等通用算法。3.3 动态追踪与入口锁定静态分析提供了线索但真正的算法调用发生在运行时。我们需要结合抓包和动态调试来定位。首先启动Charles和配置好代理的iOS设备打开“某信营业厅”App进行登录、查询话费等关键操作。观察Charles中捕获的请求寻找特征参数重点关注请求的URL、Header尤其是Authorization、X-Sign、X-Timestamp等自定义字段和Body。一个典型的签名参数可能看起来像是一串毫无规律的十六进制字符串或Base64编码的字符串。对比请求连续发起两次相同的操作如查询余额观察哪些参数是变化的如时间戳timestamp、随机数nonce哪些是固定的。变化的参数很可能参与了签名计算。假设我们发现了一个关键的POST请求其URL为https://xxx.10010.com/api/queryBalance请求体中有一个sig字段值像是Base64编码的。我们的目标就是找到生成这个sig值的代码位置。这时Frida就该上场了。我们可以编写一个简单的脚本Hook所有我们怀疑的类的方法。例如如果我们静态分析发现了一个SecurityManager类里面有一个 (NSString *)generateSignatureWithParams:(NSDictionary *)params timestamp:(long long)ts;方法就可以用Frida去Hook它// trace_sign.js Interceptor.attach(ObjC.classes.SecurityManager[ generateSignatureWithParams:timestamp:].implementation, { onEnter: function(args) { console.log(\[*] SecurityManager.generateSignatureWithParams called!\); // 打印参数 var params new ObjC.Object(args[2]); // self, SEL, params... var timestamp args[3]; console.log(\Params: \ params.toString()); console.log(\Timestamp: \ timestamp); }, onLeave: function(retval) { console.log(\[*] Return value: \ new ObjC.Object(retval)); } });使用命令frida -U -l trace_sign.js -f com.xxx.ChinaUnicom --no-pause来启动App并注入脚本。当进行查询操作时如果这个函数被调用我们就能在控制台看到详细的输入输出从而确认它是否就是生成签名的函数。4. 核心算法逆向与代码还原一旦锁定了关键的函数最核心、最耗时的部分就开始了理解并还原算法逻辑。4.1 反汇编与伪代码分析我们将目标二进制文件ChinaUnicom用Hopper或IDA打开并导航到我们感兴趣的函数。例如找到了-[SecurityManager generateSignatureWithParams:timestamp:]这个方法的地址。在反汇编视图中我们会看到ARM64汇编指令。对于复杂的算法直接读汇编效率很低。IDA Pro的F5功能生成伪代码是我们的救星。按下F5后我们会得到一段类似C语言的伪代码。虽然变量名是自动生成的如v1,v2,a1但逻辑结构清晰可见。分析伪代码时要像侦探一样识别标准库函数伪代码中会调用很多函数如_CC_SHA256、_CCCrypt、_malloc、_memcpy等。CC开头的一般是CommonCrypto库的函数这是iOS系统的加解密库。通过函数名和参数可以判断它在进行哪种操作SHA256哈希、AES加密等。跟踪数据流关注函数的输入参数params字典timestamp是如何被处理的。它们可能被拼接成一个字符串然后进行哈希也可能被转换成JSON再与一个密钥进行某种运算。查找常量与字符串算法中常常硬编码hardcode一些密钥、盐值salt、或固定的字符串。在IDA的字符串窗口ShiftF12搜索伪代码中出现的十六进制常量或字符串片段可能会直接找到key“#%Unicom2024!*”这样的明文字符串。这往往是突破的关键。还原逻辑将伪代码的逻辑用自己熟悉的语言如Python重新实现一遍。在这个过程中需要猜测和验证那些自动生成的变量名所代表的真实含义。4.2 算法模式识别与验证根据经验移动端签名算法无外乎几种常见模式模式A参数排序密钥拼接哈希将所有请求参数不包括sign本身按字典序Key排序。拼接成key1value1key2value2...的字符串。在字符串末尾拼接一个固定的密钥appSecret。对整个字符串进行MD5或SHA256哈希得到签名。模式BHMAC算法直接使用CommonCrypto的CCHmac函数使用一个密钥appSecret对消息可能是排序后的参数字符串或原始Body进行哈希运算HMAC-SHA256。模式C自定义加密可能先对参数做哈希再将哈希结果与时间戳、设备ID等组合进行一次AES加密最后输出Base64。在伪代码中如果你看到了CC_MD5_Init、CC_MD5_Update、CC_MD5_Final这一系列调用那就是MD5算法。如果看到CCHmac那就是HMAC。如果看到CCCrypt且第二个参数是kCCEncrypt或kCCDecrypt那就是在进行AES/DES等对称加密。验证算法这是最关键的一步。用我们还原的Python代码使用从抓包中获取的原始请求参数params和timestamp计算出一个签名值。然后与抓包中实际的sig字段进行比对。如果一致恭喜你算法还原成功如果不一致就需要回头检查是否漏掉了某个固定参数如appKey参数的排序规则是否正确密钥找对了么哈希结果是否做了Base64或Hex编码的转换4.3 对抗混淆与加固商业App尤其是金融、运营商类应用很可能使用了代码混淆或商业加固方案如腾讯御安全、网易易盾、梆梆加固等。这会给逆向带来巨大困难控制流扁平化将正常的if-else、switch分支结构打乱用switch和goto实现使逻辑难以理解。字符串加密所有硬编码的字符串包括密钥在二进制文件中都是加密的运行时动态解密。符号表去除二进制文件中的函数名、类名等符号信息被剥离class-dump可能失效或只能导出很少的信息。反调试检测应用会检测是否被附加调试器ptrace、sysctl等一旦发现就崩溃或执行错误逻辑。应对策略动态获取对于字符串加密可以在运行时当字符串被解密后使用Frida的Interceptor去Hook内存读写函数或Objective-C的字符串创建方法如[NSString stringWithCString:encoding:]直接获取明文字符串。耐心分析对于控制流混淆没有捷径只能结合动态调试单步跟踪程序的执行流程在关键分支点记录寄存器的值慢慢理清逻辑。IDA的图形视图空格键切换有时能帮助理解大致的块结构。绕过反调试使用Frida脚本或越狱插件如AntiAntiDebug来Hook反调试函数的检测点使其总是返回“安全”的结果。5. 算法复现与自动化脚本编写成功逆向出算法后我们就可以用高级语言如Python将其复现从而脱离iOS环境在任何地方模拟客户端的请求。5.1 Python复现核心算法假设我们分析出的算法是模式A排序拼接MD5。下面是一个Python示例import hashlib import time import urllib.parse class UnicomSigner: def __init__(self, app_key, app_secret): self.app_key app_key self.app_secret app_secret def generate_sign(self, params: dict, timestamp: int None) - str: 生成签名 :param params: 请求参数字典不包含sign本身 :param timestamp: 时间戳秒级如果为None则使用当前时间 :return: 签名字符串小写MD5 if timestamp is None: timestamp int(time.time()) # 1. 添加固定参数 sign_params params.copy() sign_params[appKey] self.app_key sign_params[timestamp] str(timestamp) # 注意服务器可能要求字符串格式 # 2. 按Key字典序排序 sorted_items sorted(sign_params.items(), keylambda x: x[0]) # 3. 拼接成 key1value1key2value2... 的格式 # 注意value可能需要URL编码根据实际情况调整 query_string .join([f{k}{urllib.parse.quote(str(v))} for k, v in sorted_items]) # 4. 拼接密钥 string_to_sign query_string self.app_secret # 5. 计算MD5或SHA256 m hashlib.md5() m.update(string_to_sign.encode(utf-8)) sign m.hexdigest().lower() # 注意大小写服务器可能区分 return sign # 使用示例 signer UnicomSigner(app_keyyour_app_key, app_secretyour_app_secret2024) params {phone: 13800138000, action: queryBalance} timestamp 1685952000 signature signer.generate_sign(params, timestamp) print(fGenerated Sign: {signature})关键细节编码问题确保拼接字符串时的编码与iOS端一致通常是UTF-8。参数顺序字典序排序必须严格按照ASCII码顺序。值处理数字是否要转换成字符串布尔值true/false如何处理空值null或空字符串是否参与签名这些细节必须与目标App完全一致一个字符的差异都会导致签名错误。哈希输出MD5结果是32位十六进制字符串服务器要求大写还是小写或者是Base64编码的16字节二进制数据5.2 构建完整的请求模拟有了签名算法我们就可以用requests库模拟完整的请求了。import requests import json def make_unicom_request(api_url, params, app_key, app_secret): signer UnicomSigner(app_key, app_secret) timestamp int(time.time()) sign signer.generate_sign(params, timestamp) # 构造最终请求体通常签名sign和timestamp会作为单独字段加入 final_payload params.copy() final_payload[sign] sign final_payload[timestamp] timestamp # 可能还有其他固定字段如appKey, version等 final_payload[appKey] app_key final_payload[version] 8.0.0 headers { User-Agent: ChinaUnicom/8.0.0 (iPhone; iOS 15.4; Scale/3.00), Content-Type: application/json; charsetutf-8, # 也可能是 application/x-www-form-urlencoded } # 根据实际情况选择POST或GET if api_url.endswith(queryBalance): # 假设这个接口是form-data格式 resp requests.post(api_url, datafinal_payload, headersheaders) else: # 假设其他接口是json格式 resp requests.post(api_url, jsonfinal_payload, headersheaders) return resp.json()这个模拟请求需要考虑的细节非常多User-Agent是否需要与设备绑定Content-Type是application/json还是application/x-www-form-urlencoded请求头里是否需要额外的设备指纹字段如X-Device-ID,X-IMSI这些信息往往需要从第一次启动App或登录时的请求中捕获。5.3 处理动态密钥与会话更复杂的情况是签名密钥appSecret不是硬编码的而是在登录或初始化时从服务器动态获取的。或者整个通信过程使用了类似OAuth的令牌机制签名是基于access_token来计算的。应对策略逆向登录流程首先完整地逆向用户登录的接口。这个接口可能会返回一个sessionKey、token或一个加密的appSecret。你需要分析客户端是如何使用返回的数据来初始化后续的签名算法的。Hook网络库使用Frida Hook网络请求的发送函数如AFNetworking的-[AFHTTPSessionManager dataTaskWithRequest:completionHandler:]在请求发出前打印出完整的请求对象观察登录成功后后续请求的Header或Body中多了哪些字段。模拟完整流程你的自动化脚本需要先模拟登录从响应中提取出关键令牌或密钥缓存起来再用于后续业务请求的签名计算。这使脚本从一个简单的算法复现升级成了一个能够维持会话状态的“机器人”。6. 逆向过程中的常见问题与排查实录在实际操作中你一定会遇到各种各样的问题。下面是我在分析“某信营业厅”及类似App时踩过的一些坑和解决方案。6.1 抓包无数据或HTTPS解密失败现象Charles能看到CONNECT请求但看不到具体的HTTP/HTTPS请求内容或者直接显示SSL Proxying not enabled for this host。排查证书问题确保Charles的根证书已在iOS设备上安装且被完全信任。在iOS的“设置-通用-关于本机-证书信任设置”里找到Charles Proxy CA打开完全信任开关。这是最常见的原因。SSL Pinning应用使用了证书绑定。尝试使用SSL Kill Switch 2通过Cydia安装全局禁用。如果无效需要写Frida脚本精确Hook。一个通用的脚本是HookNSURLSession的- URLSession:didReceiveChallenge:completionHandler:方法或者HookSecTrustEvaluate函数强制返回验证成功。非HTTP(S)协议有些应用可能使用自定义的TCP或基于WebSocket的二进制协议这些流量Charles默认无法解析。心得遇到抓不到包的情况先别急着怀疑人生99%是证书或Pinning的问题。装好SSL Kill Switch 2能解决大部分问题。如果还不行用Frida脚本大法。6.2 Class-Dump失败或导出信息过少现象执行class-dump后只导出几十个头文件或者报错。排查未砸壳最可能的原因是你分析的二进制文件还是加密状态。用otool -l 二进制文件 | grep -A 4 -B 2 cryptid命令查看cryptid字段如果为1则表示加密需要砸壳。加固/混淆应用使用了商业加固方案剥离或混淆了符号表。class-dump对这类文件无能为力。Swift项目class-dump主要针对Objective-C运行时。对于纯Swift或Swift为主的项目导出的信息会非常有限。需要使用dsdump来自jtool2等工具来解析Swift符号。解决方案首先确认文件已砸壳。对于加固应用可以尝试使用frida-ios-dump在应用运行时dump内存有时能得到更完整的镜像。对于Swift动态分析Frida, Cycript比静态分析更有效。6.3 算法还原后签名验证不通过现象用自己还原的Python代码计算出的签名与抓包中的真实签名不一致。排查步骤逐步缩小范围参数完整性确认参与签名的参数字典是否完全一致。除了明面的phone、action是否还有隐藏的全局参数如version、platform、channel这些参数可能由App在启动时全局设置每次请求自动附加。Hook网络层发起函数打印出最终发出的参数字典与你的对比。参数格式数字100和字符串100的签名结果天差地别。服务器要求的是什么格式时间戳是10位还是13位是数字还是字符串仔细对比抓包中原始请求的BodyRaw或Text视图。排序规则确认排序是按键Key的字典序lexicographical orderASCII码值排序。Python的sorted(dict.items())默认是按key排序但务必确认顺序与iOS端一致。一个技巧是在Frida Hook的签名函数里把参与签名的参数字典排序后完整打印出来。拼接格式键值对之间是用连接还是用|键和值之间是用还是:末尾是否有换行符\n这些分隔符必须一模一样。编码问题值是否需要URL编码iOS端使用的编码标准是什么如果值中包含中文或特殊字符这一点至关重要。尝试在拼接前对每个值进行urllib.parse.quote(str(v))处理。密钥与盐你使用的appSecret是否正确它可能不是简单的字符串而是经过某种变换如Base64解码后的二进制数据。算法中是否还拼接了其他的固定盐值salt哈希算法与输出你确定是MD5吗会不会是SHA1或SHA256哈希后的二进制结果是直接转换成16进制小写字符串还是先做Base64编码有些算法还会对哈希结果进行二次处理比如截取前16位或者再进行一次哈希。终极验证方法在Frida脚本中不仅打印签名函数的输入参数还在函数内部在关键步骤如拼接完字符串后、计算哈希前将中间字符串也打印出来。然后在你Python代码的相同位置打印中间字符串进行逐字符比对。这是最笨但最有效的方法。6.4 应用崩溃或行为异常现象一注入Frida脚本或者一附加LLDB调试器App就闪退。原因应用内置了反调试、反注入检测。解决方案Frida反检测使用Frida的-f参数以spawn方式启动应用而不是attach到已运行的进程。或者使用Frida的--no-pause选项。也可以使用专门对抗反Frida的脚本这些脚本会Hook常见的检测点如dlopen,dlsym,sysctl,ptrace等。调试器检测对于ptrace的PT_DENY_ATTACH可以写一个Frida脚本或tweak在ptrace函数被调用时如果是PT_DENY_ATTACH请求就跳过它。越狱检测应用可能检测设备是否越狱检查/Applications/Cydia.app是否存在、尝试写入/private目录等。可以使用Liberty Lite越狱插件或Shadow这类工具来隐藏越狱环境。心得反调试是一场攻防战。如果通用插件无效就需要静下心来逆向分析App的反调试代码具体在哪里然后写针对性的Hook脚本来绕过。这本身也是逆向工程中极具挑战和乐趣的一部分。整个逆向分析“某信营业厅”算法的过程就像是在解一个多维度的谜题。它考验的不仅仅是技术更是耐心、细心和系统化的思维。从环境搭建、信息收集到关键代码定位、算法还原再到最后的复现与验证每一步都可能遇到意想不到的障碍。但每解决一个难题你对iOS系统、对密码学、对网络协议、对软件保护的理解就会更深一层。这份经验是任何教科书都无法给予的。最后记住技术是把双刃剑所有的分析和研究都应在法律允许和道德约束的范围内进行用于提升自身安全能力而非不当用途。