1. 项目概述与核心目标最近在研究移动端的数据抓取和接口分析发现很多主流App尤其是像今日头条这样体量的应用对核心业务接口的保护越来越严密。传统的抓包工具往往只能看到一堆加密的乱码这让很多数据分析、竞品研究或者自动化脚本的开发者感到头疼。我手头正好有一台越狱的iPhone就想着能不能以“今日头条”的搜索接口为靶子完整走一遍iOS逆向分析流程把它的加密逻辑给扒出来。这个项目的目的很纯粹不是教你做坏事而是通过一个真实的、有代表性的案例掌握一套分析iOS应用网络通信加密的通用方法论。无论你是安全研究员、爬虫工程师还是对移动端底层原理感兴趣的开发者这套从环境搭建、静态分析到动态调试的完整思路都能让你在面对一个“黑盒”App时知道从哪里下手。整个实战会围绕“搜索”这个高频功能展开。当你打开今日头条App在顶部的搜索框输入关键词时App会向服务器发送一个请求来获取搜索结果。我们的目标就是拦截这个请求搞清楚它发送的数据比如搜索词、页码、设备信息等是如何被加密的以及服务器返回的数据又是如何被解密的。最终我们希望能在电脑上用Python或任何其他语言模拟出这个完整的加密解密过程从而能够脱离App本身自由地调用这个搜索接口。这个过程会涉及到越狱环境配置、砸壳获取可分析二进制文件、静态分析寻找关键函数、以及使用Frida进行动态Hook验证等多个环节我会把每个环节的细节、踩过的坑和心得都详细记录下来。2. 逆向环境与工具链搭建工欲善其事必先利其器。iOS逆向对环境的依赖度比较高一个稳定、齐全的工具链是成功的一半。下面我会分步骤说明如何搭建这个“作战指挥部”。2.1 设备与系统准备首先你需要一台已越狱的iOS设备。这是iOS逆向的物理基础。我使用的是iPhone 7系统是iOS 14.4。选择这个版本是因为其越狱方案如unc0ver相对成熟稳定。不建议使用最新版本的iOS因为越狱工具和兼容的逆向工具往往有滞后性。设备越狱后最重要的就是安装包管理器Cydia或它的现代替代品如Sileo。通过Cydia我们可以安装后续所需的各种命令行工具和框架。在电脑端我使用的是macOS系统因为很多iOS开发工具链如Xcode命令行工具在macOS上集成得最好。Windows用户也可以通过虚拟机安装macOS或者尝试一些跨平台的替代工具但可能会遇到更多环境配置问题。2.2 核心工具安装与配置接下来通过Cydia在越狱设备上安装以下核心工具OpenSSH允许我们通过SSH协议从电脑远程登录到iPhone在终端里执行命令。安装后务必立即修改默认密码alpine否则你的设备在联网状态下将极度不安全。Frida这是我们本次实战的“王牌”。它是一个动态插桩工具可以让我们在App运行时注入自己的JavaScript脚本来Hook挂钩特定的函数查看、修改参数和返回值。在Cydia中添加Frida的官方源https://build.frida.re然后安装Frida包。Cydia Substrate或它的继任者libhooker取决于你的越狱工具这是许多注入工具包括老版本的Frida的底层框架。虽然新版Frida可以独立工作但安装它能确保更好的兼容性。adv-cmds包含ps等命令方便查看系统进程。在电脑macOS上我们需要安装Python 3通过Homebrew安装即可brew install python3。Frida客户端在电脑终端执行pip3 install frida-tools。安装完成后运行frida-ps -U命令如果能看到你手机上的进程列表说明电脑与手机的Frida连接成功。砸壳工具从App Store下载的App都是经过苹果加密的俗称“加壳”我们无法直接分析。需要砸壳。推荐使用frida-ios-dump。这是一个基于Frida的砸壳脚本。将其克隆到本地git clone https://github.com/AloneMonkey/frida-ios-dump.git然后按照其README安装依赖主要是pip3 install -r requirements.txt。之后需要配置dump.py脚本中的SSH连接信息你的手机IP和密码。反汇编与静态分析工具Hopper Disassembler或IDA Pro强大的反汇编工具可以将二进制文件转换成可读的汇编代码并尝试生成伪代码。Hopper有体验版对于初学者足够。class-dump专门用于导出iOS应用中Objective-C类的头文件。这对于快速了解App的类结构非常有帮助。通过Homebrew安装brew install class-dump。MachoOView一个查看Mach-O文件iOS可执行文件格式结构的图形化工具可以方便地查看加密状态、加载命令等信息。注意环境搭建过程可能因iOS版本和越狱工具不同而略有差异。如果遇到问题多查阅相关工具在GitHub上的Issue页面通常都能找到解决方案。一个常见的坑是Frida连接失败请确保手机和电脑在同一个局域网并且电脑端的Python环境是Python 3。3. 目标应用分析与砸壳环境准备好后我们正式向“今日头条”App开刀。第一步是获取一个可以静态分析的可执行文件。3.1 定位与砸壳操作首先在越狱手机上打开今日头条App。然后在电脑终端通过SSH连接到手机ssh root[你的手机IP]。连接成功后输入ps -A | grep News今日头条的Bundle Identifier通常包含News来找到今日头条的进程IDPID。记下这个PID。接着我们使用之前配置好的frida-ios-dump进行砸壳。在电脑的frida-ios-dump目录下运行python3 dump.py [PID]。脚本会自动完成内存dump和修复最终在当前目录生成一个名为[App名称].ipa的文件。这个.ipa文件本质上是一个压缩包解压后在Payload/[App名称].app/目录下就能找到我们梦寐以求的、已砸壳的主二进制可执行文件通常名字就是App的名称。如何验证砸壳成功用MachoOView打开这个二进制文件查看Load Commands部分找到LC_ENCRYPTION_INFO或LC_ENCRYPTION_INFO_64。如果其中的CryptID字段值为0恭喜你砸壳成功。如果为1则表示仍然加密需要检查砸壳过程。3.2 初步静态分析类结构探查拿到砸壳后的二进制文件先不急着深入汇编代码。我们可以用class-dump来快速窥探一下App的内部类结构这能给我们提供宝贵的方向。在终端执行class-dump -H [二进制文件路径] -o [输出头文件目录]。这个命令会将二进制文件中所有Objective-C类的头文件导出到指定目录。浏览这些头文件我们可以搜索与“搜索”、“网络”、“加密”、“请求”相关的关键词如SearchRequestAPIEncryptDecryptHTTPManager等。例如我们可能会发现名为TTSearchRequestModel、TTNetworkManager、TTCryptoUtility之类的类。这些类名就像地图上的路标为我们后续的逆向分析指明了潜在的关键区域。尤其是那些看起来像是管理网络请求或执行加密解密的类需要重点标记。4. 动态分析与加密逻辑定位静态分析提供了线索但加密逻辑往往在运行时才完全展现。动态分析是我们破解加密的核心手段Frida在这里大显神威。4.1 抓包确定目标接口首先我们需要知道今日头条搜索接口的具体地址和大致参数结构。在电脑上启动抓包工具如Charles或mitmproxy并在手机上配置好代理。然后在今日头条App内进行一次搜索操作。抓包结果可能会显示一个类似https://is.snssdk.com/api/search/...的请求。查看其请求体Request Body你很可能看到的不是明文的keywordxxx而是一串看似随机的Base64编码字符串或者直接是二进制数据。同时请求头Headers里可能会包含一些自定义字段如X-SS-STUB、X-Gorgon、X-Khronos等这些通常是签名或加密相关的参数。我们的目标就是找出生成这个加密请求体和这些神秘请求头的代码在哪里。4.2 使用Frida进行函数Hook假设我们从抓包看到请求体是Base64编码的解密后可能是一个JSON。那么在App中必然有一个函数在发送请求前将原始的搜索参数字典或对象转换成了这个Base64字符串。同样也必然有函数在生成X-Gorgon这类签名。我们可以利用Frida来Hook那些常见的、用于Base64编码和网络请求的函数。第一步编写Frida脚本创建一个名为search_hook.js的文件内容如下// 首先Hook常见的Base64编码方法 var base64Encode ObjC.classes.NSString[- base64EncodedString]; if (base64Encode) { Interceptor.attach(base64Encode.implementation, { onEnter: function(args) { // args[0]是selfargs[1]是selectorargs[2]才是函数参数如果有 // 这个函数没有显式参数但我们可以打印调用栈和self的内容 console.log([] NSString base64EncodedString called!); // 打印调用栈帮助定位上层函数 console.log(Backtrace:\n${Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(\n)}); // 尝试打印self的内容原始的NSString var selfStr ObjC.Object(args[0]).toString(); console.log(Self (input string): ${selfStr.substring(0, 100)}...); // 只打印前100字符 }, onLeave: function(retval) { // retval是编码后的Base64字符串 var encodedStr ObjC.Object(retval).toString(); console.log(Return (encoded string): ${encodedStr.substring(0, 100)}...); } }); } // 其次Hook网络层。今日头条很可能使用AFNetworking或自研网络库。 // 我们可以尝试Hook NSURLSession的任务创建或请求设置方法。 var NSURLSession ObjC.classes.NSURLSession; // 尝试Hook一个常用的创建任务的方法例如 dataTaskWithRequest: if (NSURLSession NSURLSession[- dataTaskWithRequest:completionHandler:]) { var dataTaskWithRequest NSURLSession[- dataTaskWithRequest:completionHandler:]; Interceptor.attach(dataTaskWithRequest.implementation, { onEnter: function(args) { // args[0]: self, args[1]: _cmd, args[2]: request, args[3]: completionHandler var request new ObjC.Object(args[2]); var url request.URL(); var headers request.allHTTPHeaderFields(); var httpBody request.HTTPBody(); // 过滤出搜索相关的请求 if (url.toString().indexOf(search) ! -1) { console.log(\n 捕获到搜索请求 ); console.log(URL: ${url}); console.log(Headers: ${JSON.stringify(headers)}); if (httpBody) { var bodyStr ObjC.classes.NSString.alloc().initWithData_encoding_(httpBody, 4); // 4 for NSUTF8StringEncoding console.log(HTTP Body: ${bodyStr}); } // 同样打印调用栈找到是谁发起的这个请求 console.log(Backtrace:\n${Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(\n)}); } } }); }第二步注入并运行脚本在电脑终端运行frida -U -f com.ss.iphone.article.News -l search_hook.js --no-pause。这条命令会启动今日头条App-f指定Bundle ID并注入我们的脚本。第三步触发搜索并观察在手机App上进行一次搜索。此时电脑终端会疯狂刷出日志。我们需要在日志中寻找与我们抓包看到的Base64字符串或特殊Header如X-Gorgon相关的输出。当Hook到生成我们看到的那个Base64字符串的函数时Frida会打印出调用栈Backtrace。调用栈就像一份“犯罪现场”的足迹记录从下往上读显示了是哪些函数一步步调用了这个Base64编码函数。栈顶附近的函数名很可能就是今日头条内部封装加密逻辑的关键函数。例如你可能会在调用栈中看到[TTSearchRequestEncoder encodeRequest:]这样的函数名这几乎就是我们要找的“加密器”了。4.3 定位关键加密函数通过动态Hook我们可能定位到了一个或多个可疑的函数比如encodeRequest:、generateSignatureWithParams:、encryptBody:等。接下来我们需要回到静态分析工具Hopper或IDA加载砸壳后的二进制文件直接搜索这些函数名。在Hopper中按CmdG跳转到地址或直接搜索函数名。找到函数后可以查看它的汇编指令更强大的是使用Hopper的伪代码生成功能按F5键。伪代码虽然不完全准确但极大提升了可读性能让我们以近似C语言的逻辑来理解这个函数做了什么。分析这个encodeRequest:函数你可能会发现它接收一个包含搜索参数关键词、位置、时间戳等的字典作为输入。按照某种固定格式可能是JSON序列化这个字典。调用一个AESEncrypt或CCCryptiOS系统加密API函数对序列化后的字符串进行加密。将加密后的二进制数据转换成Base64字符串。同时可能还会调用另一个函数generateGorgon利用时间戳、设备ID、加密后的Body等参数通过HMAC-SHA256等算法计算出一个签名赋值给X-Gorgon请求头。至此加密的核心逻辑和位置就被我们找到了。5. 加密算法逆向与模拟实现找到关键函数后最硬核的部分来了逆向其具体的加密算法和密钥。5.1 分析加密模式与密钥在Hopper的伪代码中仔细查看调用加密函数如CCCrypt的部分。这个函数调用会包含几个关键参数op操作类型kCCEncrypt加密或kCCDecrypt解密。alg算法如kCCAlgorithmAES。options选项这里藏着加密模式如kCCOptionPKCS7Padding和模式如kCCModeCBC。kCCOptionPKCS7Padding | kCCModeCBC就表示CBC模式PKCS7填充。key和keyLength密钥和其长度。iv和ivLength初始化向量IV和其长度如果是CBC等模式。密钥和IV的来源是重点。它们可能是硬编码在二进制文件里在伪代码中可能是一个字符数组如char key[] “1234567890abcdef”;。你可以尝试在二进制文件中搜索字符串在Hopper中按CmdAltS查找可能的密钥。从某个服务器接口动态获取这就需要你Hook网络请求找到获取密钥的接口。由其他参数通过固定算法生成例如用设备ID的MD5值的前16位作为密钥。在我们的动态Hook日志中如果之前打印了调用栈栈中加密函数附近的函数可能会揭示密钥的生成逻辑。有时密钥甚至就作为参数传递给了加密函数在onEnter回调中可以直接打印出来。5.2 使用Frida主动调用与验证在不确定算法细节时Frida的Interceptor还有一个强大功能不仅能在函数被调用时打印信息还能修改函数的参数或返回值。但更直接的方法是使用Frida的NativeFunction来主动调用我们找到的加密函数。假设我们已经确定了加密函数encryptWithKey:的地址可以通过Module.findExportByName获取并且知道了它的参数和返回值类型通过分析伪代码的签名猜测我们可以写一个脚本在App运行时直接调用这个函数传入我们构造的明文看它返回的密文是否和我们抓包得到的一致。// 假设我们找到了加密函数的地址偏移是 0x123456 (来自Hopper) var baseAddr Module.findBaseAddress(今日头条); // 获取ASLR偏移后的基地址 var encryptFuncAddr baseAddr.add(0x123456); // 根据伪代码猜测函数原型void* encrypt(void* data, size_t dataLen, void* key); var encryptFunc new NativeFunction(encryptFuncAddr, pointer, [pointer, size_t, pointer]); // 准备测试数据 var testStr “hello world”; var testData Memory.allocUtf8String(testStr); var keyStr “my_test_key_16b”; // 假设的密钥 var keyData Memory.allocUtf8String(keyStr); // 调用函数 var resultPtr encryptFunc(testData, testStr.length, keyData); // 读取结果...如果调用成功且结果符合预期那就证明我们找对了函数和密钥。这个过程可能需要反复尝试调整函数签名和参数。5.3 使用Python还原算法一旦我们通过静态分析和动态验证完全掌握了加密算法例如AES-128-CBCPKCS7填充固定的密钥和IV以及签名算法例如对“URL路径时间戳加密Body”的HMAC-SHA256就可以在电脑上用Python或其他语言进行复现了。import base64 import json import time import hashlib import hmac from Crypto.Cipher import AES from Crypto.Util.Padding import pad class ToutiaoSearchCracker: def __init__(self): # 这些密钥和IV是通过逆向分析得到的此处为示例并非真实值 self.aes_key b1234567890abcdef # 16字节密钥 self.aes_iv babcdefghijklmnop # 16字节IV self.sign_secret byour_sign_secret_here # 签名密钥 def encrypt_body(self, params_dict): 模拟App内对请求体的加密过程 # 1. 序列化参数为JSON字符串 json_str json.dumps(params_dict, separators(,, :), ensure_asciiFalse) # 确保是bytes data json_str.encode(utf-8) # 2. AES-128-CBC加密 cipher AES.new(self.aes_key, AES.MODE_CBC, self.aes_iv) encrypted_data cipher.encrypt(pad(data, AES.block_size)) # 3. Base64编码 encrypted_b64 base64.b64encode(encrypted_data).decode(utf-8) return encrypted_b64 def generate_gorgon(self, url_path, timestamp, encrypted_body): 模拟生成X-Gorgon签名 # 假设签名算法为HMAC-SHA256(secret, url_path str(timestamp) encrypted_body) message f{url_path}{timestamp}{encrypted_body}.encode(utf-8) signature hmac.new(self.sign_secret, message, hashlib.sha256).hexdigest() # 可能还会经过一些格式化比如取前几位等 return signature.upper()[:16] # 示例实际格式需分析 def build_search_request(self, keyword, page1): 构建一个完整的搜索请求 # 构造原始参数 raw_params { keyword: keyword, offset: (page - 1) * 20, count: 20, source: search_tab, # ... 其他必要参数 } # 加密请求体 encrypted_body self.encrypt_body(raw_params) # 生成签名所需参数 timestamp int(time.time()) url_path /api/search/v1/ # 生成签名 x_gorgon self.generate_gorgon(url_path, timestamp, encrypted_body) # 构造最终请求头 headers { Content-Type: application/x-www-form-urlencoded, # 可能是其他类型 X-Gorgon: x_gorgon, X-Khronos: str(timestamp), # ... 其他固定Header } # 构造请求数据加密后的Body可能以特定字段名传输 request_data fencrypted_data{encrypted_body} return url_path, headers, request_data # 使用示例 cracker ToutiaoSearchCracker() url, headers, data cracker.build_search_request(逆向工程) print(fURL: {url}) print(fHeaders: {headers}) print(fData: {data}) # 接下来就可以用requests库发送这个请求了6. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到各种问题。下面是我踩过的一些坑和解决方法。6.1 Frida注入失败或脚本不生效现象frida-ps -U能看到设备但注入时提示Failed to spawn: unable to find process with name ‘xxx’或脚本无任何输出。排查检查Bundle ID确保-f参数后的Bundle ID正确。可以用frida-ps -U | grep News来确认。检查Frida版本确保手机端和电脑端的Frida版本兼容。尽量使用相近版本。重启Frida服务在手机端可以通过Cydia重新安装Frida包或使用命令killall frida-server后重新启动它如果它以服务形式运行。关闭App沙盒限制有些越狱环境或App有更强的反调试。可以尝试使用frida -U --no-pause -f com.xxx.xxx中的--no-pause或在脚本中使用setTimeout延迟Hook避免在App启动早期注入失败。6.2 Hook不到目标函数现象脚本成功注入App也能运行但搜索时没有打印出我们预期的加密或网络请求日志。排查函数签名错误Objective-C的函数名Selector包含冒号。base64EncodedString和base64EncodedStringWithOptions:是两个不同的函数。在Hopper或class-dump的头文件里确认准确的函数名。调用时机问题加密可能发生在子线程或者你的Hook代码执行时某些类尚未加载。可以尝试Hook类的load方法或_objc_init然后在回调中再动态去Hook目标函数。App使用了自定义加密或第三方库App可能没有使用系统的NSString的Base64方法而是用了自己的实现或第三方库如OpenSSL。你需要扩大搜索范围在静态分析时关注那些看起来像加密操作的函数调用如CC_SHA256,EVP_EncryptUpdate等。6.3 静态分析伪代码难以理解现象Hopper生成的伪代码逻辑混乱充斥着大量临时变量和指针操作看不懂。技巧重命名变量和函数在Hopper中你可以双击一个变量或函数给它起一个有意义的名字如inputString,encryptionKey。这能极大提升代码可读性。关注核心系统调用不要试图理解每一行汇编。重点关注对已知系统API的调用如CCCrypt,CC_SHA256,malloc,memcpy等。这些是理解算法逻辑的锚点。动态调试辅助在Frida脚本中除了打印参数还可以打印某个对象的所有属性ObjC.Object(args[0]).$ivars或调用其方法来辅助理解数据结构。6.4 算法还原后请求仍被服务器拒绝现象你用Python模拟的请求加密和签名都看似正确但服务器返回错误码如403、412。排查参数完整性检查你的请求是否包含了所有必要的参数。对比抓包到的原始请求一个都不要少包括那些看似无关的字段如_rticket,device_id,iid等。有些参数可能参与签名计算。编码与格式确认JSON序列化的格式空格、缩进、键序是否与App完全一致。有些签名算法对字符串的格式极其敏感。可以尝试将App加密前的原始字符串通过Frida dump出来与你生成的字符串进行逐字节比较。签名算法细节签名算法可能比你想象的复杂。例如参与签名的字符串可能经过了URL编码或特定的规范化处理。再次仔细分析生成签名的函数看是否有额外的步骤如对某些字段先进行MD5哈希。时间戳同步X-Khronos通常是秒级时间戳。确保你的服务器时间与今日头条的服务器时间大致同步误差不要太大如几分钟内。TLS指纹或证书绑定高级别的防护可能会检查客户端的TLS指纹如JA3或使用SSL Pinning。这种情况下简单的requests库会被识别。你需要使用更底层的库如curl_cffi来模拟iOS的TLS指纹或者使用frida在App内直接调用其网络层函数来发送请求完全绕过客户端模拟。逆向工程是一场与开发者斗智斗勇的持久战。今日头条的加密机制也可能随时更新。今天有效的方法明天可能就失效了。因此掌握这套分析思路和工具链比记住某个具体的密钥或算法更为重要。当加密方式改变时你可以快速地重复上述流程定位新的加密函数分析新的逻辑。这个过程本身就是对iOS系统、网络协议和安全攻防的深刻学习。