ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

逆向iTunes登录协议:3DES加密分析与Python自动化实现

2026/10/8 15:29:59 拓冰建站 浏览量
逆向iTunes登录协议:3DES加密分析与Python自动化实现 1. 这次逆向的前提与目标为什么需要动iTunes登录协议1.1 业务场景自动化测试中的登录瓶颈我最初遇到这个问题是在负责一批iOS设备自动化测试任务的时候。测试过程需要反复用真实Apple ID登录iTunes、App Store验证购买、下载、账号切换等完整链路。最开始方案很朴素用Appium或XCTest驱动界面在输入框里填账号密码点击登录按钮。跑通一两次没问题一旦扩大到几十台设备并发执行问题就全出来了。UI自动化的稳定性太差。弹窗、键盘、网络慢、二次验证提示任何一个小扰动都会让脚本失败而且每台设备需要单独维护一套测试用例CI流水线跑一次要一两个小时。更关键的是登录这个动作本身变成了整个自动化链路的最不稳定环节。我当时的想法是如果能把登录协议本身搞清楚用代码直接完成握手再配合少量UI操作稳定性会高很多。这就是这次逆向的起点。所谓“逆向”不是绕开什么安全机制而是把客户端与服务器之间的协议交互过程还原出来在受控测试环境和合法授权账号下把重复性登录动作用脚本替代。最终目标很明确抓到真实的登录请求分析字段定位密码加密逻辑用Python重写然后集成到自动化测试框架里。1.2 协议逆向的基本边界在动手之前我先给自己划了几条线。只研究自己拥有或受控的测试账号只抓包分析正常登录流程不做撞库、批量注册、绕过验证码之类的事情。协议的逆向本身是安全研究里非常常见的技术方向但使用边界很重要。这次研究的场景是通过iTunes客户端或者安装在其框架上的设置程序发起一次账号登录观察客户端如何构造请求、如何加密密码、如何解析服务端返回。整个过程只需要一台测试手机、一台电脑、一个抓包工具并不需要越狱或者特殊权限也不需要绕过任何风控逻辑。提示如果你打算在自己的环境复现建议使用专门的测试设备、独立网络环境和受控账号避免影响正常使用也不要对线上服务施加过高压力。1.3 最终确定的技术路径经过最初的调研我给自己规划了一条完整路径也是本文的骨架抓包拿到完整请求 → 分析请求头和请求体的核心字段 → 重点突破password字段的加密方式3DES → 在调试器中定位密钥 → 用Python完整复刻登录请求 → 集成到自动化框架。这条路径最核心、最容易被卡住的环节就是第三、四步。因为HTTPS包虽然能解密但关键字段是加密的如果不知道加密算法的细节和密钥来源后面的所有工作都做不下去。接下来我从抓包环境开始按实际操作的顺序完整记录一遍。2. 抓包环境与关键请求的识别证书信任是最容易卡住的一步2.1 抓包工具选型与代理配置抓包工具我用过Charles、mitmproxy和Wireshark。Wireshark在HTTPS密文解密上比较费劲不太适合这个场景Charles和mitmproxy都能做SSL中间人解密差别主要在交互方式。如果你在Windows/macOS图形界面下操作Charles更直观会话列表、请求详情、重写功能、断点调试都做得很顺手。我当时是在macOS上先用Charles抓包分析锁定协议格式后面再用Python写自动化。如果你习惯命令行或者在Linux服务器上操作mitmproxy更合适配好了还能直接用mitmproxy -s xxx.py写处理脚本。无论选哪个核心配置都是两步第一让手机和电脑处于同一网络手机代理指向电脑的抓包端口Charles默认8888mitmproxy默认8080第二启用HTTPS解密。这里有个容易忽略的细节抓包工具一般默认只解密它认识的域名或者全部解密。建议在最初阶段直接开启全部SSL解密等抓到关键请求后再通过过滤缩小范围。2.2 设备端证书信任iOS必须手动信任配置代理之后手机访问HTTPS页面会提示证书不可信这是正常现象因为抓包工具给每个域名动态签发了自己的证书。Android上需要在设置里安装CA证书iOS上要复杂一些分两步iOS 10.3及以上通过Safari访问抓包工具提供的证书下载地址通常是http://charlesproxy.com/getssl或mitmproxy的http://mitm.it下载描述文件然后在“设置→通用→描述文件”里安装。安装完成后还要到“设置→通用→关于本机→证书信任设置”中把抓包工具的证书开关打开选择“完全信任”。这一步不做SSL握手依然失败抓包工具里只能看到ClientAlert看不到HTTP层内容。这个环节最容易让人误以为代理配置错了。实际表现是抓包工具里能看到大量TLS握手失败记录但看不到任何HTTP请求这就是iOS没有完全信任证书的典型特征。2.3 从海量流量中快速定位登录请求配置好抓包环境后我在手机上打开iTunes Store触发一次账号登录同时观察Charles里的请求流。流量非常多有系统更新、推送、iCloud同步等需要过滤。第一层过滤是域名。Apple的认证服务主要集中在认证网关相关的域名下具体主机名可能随地区和请求类型有差异但核心认证路径相对固定。我用域名过滤器把所有请求限定到这个范围流量立刻少了很多。第二层过滤是方法。登录请求是一个POST请求参数是表单格式响应是JSON。在Charles里按POST过滤后能看到一个关键请求。请求的路径很长包含一些版本代号看着像某种协议标识。它的请求头里带有一组X-Apple-*开头字段请求体是keyvalue格式的字符串。这个请求就是我要研究的对象。为了确认它确实对应登录动作我在Charles里点击“Repeat”重放一次对比响应结果如果服务端返回与正常登录一致的结构哪怕带错误码就说明请求被正确识别了。2.4 关键请求头的观察记录我把这个POST请求的请求头和部分请求体复制下来结构大致如下脱敏处理POST /gsa/gsc/... HTTP/1.1 Host: gsa.apple.com User-Agent: Configurator/2.15 (Macintosh; OS X 10.15.1) Accept: application/json X-Apple-Client-Id: 32位hex字符串 X-Apple-I-Client-Time: 2025-01-08T12:24:36Z X-Apple-I-MD: 一串Base64 X-Apple-I-MD-M: 一串Base64 X-Apple-Store-Front: 143465-19,32 Content-Type: application/x-www-form-urlencoded请求体里有aid、account、password、protocol、scnt、cp、sid这些字段其中account是明文账号password是一长串Base64字符。这里有两个最重要的观察结果password字段不是明文是密文后面必须重点处理。请求头中的X-Apple-I-MD和X-Apple-I-MD-M看起来也是加密相关字段但经过后续验证这两个字段在登录场景下不是必须的可以留空。如果带着旧值反而可能导致请求失败。抓包到此协议格式基本清晰了。接下来的核心任务只有一个把password字段的生成方式还原出来。3. 请求体的核心字段password字段的加密特征与定位思路3.1 抓到的请求体格式分析把请求体解码后关键的键值对如下部分字段脱敏aid: 892166055 account: test_accountexample.com password: Yp3JwBQxmdvJ4KmJZ0fO/7Jv4B5RjYtGgQ protocol: a12 scnt: 1 cp: 143465-19,32aid是设备或账号相关的整型IDaccount是邮箱账号protocol是协议版本标识scnt是某种计数参数与重试次数有关。password字段是Base64编码字符串以结尾这个填充特征说明密文长度不是4的倍数但Base64补位后变成了标准形式。将Base64解码后我数了一下字节数。如果解密后的明文是一个普通密码例如abc1234569字节密文长度通常会是8字节的倍数因为分组加密算法按块处理。这里解码后的长度为16字节而3DES的块大小正好是8字节16正好是两个块。这些特征都指向一个方向对称分组加密大概率是DES/3DES/AES。3.2 为什么首选3DES方向排查我之所以优先排查3DES是因为Apple相对早期的协议里3DES用得非常多。从公开资料和过去逆向经验来看很多认证接口的密码加密都选择了3DES-ECB或3DES-CBC而不是AES。这可能与历史兼容性和跨平台实现难度有关。3DES在密码学上已经不被推荐使用Sweet32攻击在2016年被公开后很多大厂陆续停用了它。但在某些存量协议中它仍然存在。特征也很明显加密算法标识为kCCAlgorithm3DES密钥长度是24字节分组大小是8字节填充方式通常是PKCS7。逆向时如何快速判断是不是3DES有几个线索可以参考线索判断方法密文长度是8字节的倍数解密后用PKCS7去填充尾部字节值等于填充长度密文变化特征明文改变一个字节观察密文分组变化范围ECB只变对应分组CBC后续分组全变二进制特征在客户端二进制中搜索kCCAlgorithm3DES的符号引用或3DES常量表协议历史特征早期iOS/Apple协议中3DES出现频率远高于AES3.3 加密模式与密钥来源的初步推断确定算法方向后还要解决两个问题加密模式是ECB还是CBC密钥从哪来关于模式我做了一个实验。我准备了两组密码一组是AAAA...AAAA另一组是BBBB...BBBB长度都超过16字节分别触发抓包。观察密文如果第一组密文的前8字节在两组间发生了变化而后续分组不受影响可以初步推断是ECB如果是CBC一个字节的变化会导致后续所有分组连锁变化。实际实验中密码的前8字节变了后续分组也变了这个结果一度让我以为走错了方向以为一定是CBC。但仔细分析后发现我改动的密码明文长度不一样导致整个明文的第一个分组就完全不同不能直观判断模式。后来我改成等长密码只改首字符再观察密文只有第一个分组变化后面分组保持不变。这就锁定了3DES-ECB模式。密钥来源问题相对好办。Apple客户端是自家二进制做本地协议时通常把密钥直接编译进可执行文件或资源文件中不太可能像服务端那样动态下发。所以我决定通过调试器直接在加密函数处提取密钥这是最快速、最准确的方式。4. 3DES算法还原从断点下注到Key/IV提取的全过程4.1 工具准备从IDA静态分析到动态Hook定位加密逻辑我用了两种手段静态分析和动态Hook。先说为什么需要两者结合。静态分析用的是IDA Pro打开对应的系统库或服务进程二进制搜索3DES相关符号。由于Apple系统库大量使用CoreCrypto框架函数名一般是CCCrypt或CCCryptorCreate。直接用字符串搜索可以定位到大量调用点但需要判断哪个调用点属于我们要分析的登录流程这个工作量不小。动态Hook用的是Frida。它在越狱设备或模拟器环境中可以直接注入进程拦截指定函数打印参数。这里需要注意Frida注入需要目标进程有调试权限实际测试时我是在受控测试设备上完成的。如果你只有普通未越狱设备也可以考虑在macOS上对iTunes相关二进制进程做调试思路是一样的。4.2 动态Hook CCCrypt直接观察Key与IVFrida脚本的核心逻辑是Hook住CCCrypt函数把每次调用的操作类型加密/解密、算法标识、Key、IV、输入数据都打印出来。脚本骨架大致如下// frida -U -f com.apple... -l hook_cccrypt.js var CCCrypt Module.findExportByName(null, CCCrypt); Interceptor.attach(CCCrypt, { onEnter: function(args) { this.op args[0].toInt32(); this.alg args[1].toInt32(); this.key Memory.readByteArray(args[4], parseInt(args[5])); this.keyLen args[5].toInt32(); this.iv args[6].isNull() ? null : Memory.readByteArray(args[6], 8); this.dataIn Memory.readByteArray(args[7], parseInt(args[8])); this.dataInLen args[8].toInt32(); console.log( CCCrypt ); console.log(op this.op alg this.alg); console.log(key hexdump(this.key, {length: this.keyLen})); if (this.iv) { console.log(iv hexdump(this.iv, {length: 8})); } console.log(dataIn hexdump(this.dataIn, {length: this.dataInLen})); } });触发一次登录后Frida输出里出现了几十条CCCrypt调用但大部分是系统其他功能发起的杂音很多。我加了一个过滤条件只关注alg kCCAlgorithm3DES数值为2且op kCCEncrypt数值为0的调用。过滤后符合条件的调用数量大幅减少其中一条的输入数据长度与抓包密码长度完全吻合。调用参数显示Key长度24字节符合3DES三密钥标准IV空ECB模式不需要IV输入数据明文密码输出数据长度与抓包密文一致4.3 本地验算算法与密钥的正确性验证拿到Key后我先在本地用Python的pycryptodome库做了一次验算确认抓包密文与本地加密结果完全一致from Crypto.Cipher import DES3 import base64 key bytes.fromhex(这里是24字节密钥的hex值) password 测试密码 cipher DES3.new(key, DES3.MODE_ECB) # 手动PKCS7填充 pad_len 8 - (len(password) % 8) padded password.encode(utf-8) bytes([pad_len]) * pad_len encrypted cipher.encrypt(padded) print(base64.b64encode(encrypted).decode())运行结果与Charles里抓到的password字段完全一致。到这一步算法、模式、密钥、填充方式四个要素全部确定加密逻辑还原完毕。4.4 静态分析交叉验证密钥为何固定不变动态Hook拿到结果之后我回到IDA里做了一次交叉验证目的是搞清楚这个Key是从哪里来的以及它是否会随账号或时间变化。在IDA中搜索Key的十六进制常量找到了一个明显的引用位置。该位置位于某个初始化函数中代码把一段固定常量拷贝到缓冲区之后没有经过任何网络请求或随机过程就直接用于加密。这说明这是一个硬编码的全局密钥所有客户端通用不随用户变化。这种设计在早期协议中非常常见客户端内置密钥服务器通过请求中的其他字段来识别客户端版本密钥只用来防止明文密码被简单地Base64编码而不是真正意义上的安全保护。这也是我后续实现自动化时最放心的部分——密钥固定意味着代码里只需要维护一份常量。5. Python完整实现与自动化集成从单次登录到批量任务5.1 算法复刻把抓包逻辑写成稳定代码加密算法在本地复刻完成后接下来就是把整个登录请求用Python实现。这里我给出一段完整可用的核心代码注意三个关键点密钥先转换格式再使用账号密码用UTF-8编码填充必须是PKCS7。import time import requests import base64 from Crypto.Cipher import DES3 # 固定密钥从客户端二进制中提取后按hex存储 KEY bytes.fromhex(填入24字节密钥的hex值) class iTunesLoginClient: def __init__(self, account: str, password: str): self.session requests.Session() self.account account self.password password self.headers { User-Agent: Configurator/2.15 (Macintosh; OS X 10.15.1), Accept: application/json, Content-Type: application/x-www-form-urlencoded, X-Apple-Client-Id: self._generate_client_id(), X-Apple-I-Client-Time: self._get_utc_time(), X-Apple-Store-Front: 143465-19,32, } staticmethod def _generate_client_id() - str: return hex(int(time.time() * 1000))[2:].zfill(32) staticmethod def _get_utc_time() - str: return time.strftime(%Y-%m-%dT%H:%M:%SZ, time.gmtime()) def _encrypt_password(self) - str: encoded self.password.encode(utf-8) pad_len 8 - (len(encoded) % 8) padded encoded bytes([pad_len]) * pad_len cipher DES3.new(KEY, DES3.MODE_ECB) return base64.b64encode(cipher.encrypt(padded)).decode() def login(self): payload { aid: 892166055, account: self.account, password: self._encrypt_password(), protocol: a12, scnt: 1, cp: 143465-19,32, } resp self.session.post( https://gsa.apple.com/gsa/gsc/..., datapayload, headersself.headers, timeout15, ) return resp.json()这段代码的核心价值不在于本身有多复杂而在于它把前面逆向得到的所有结论固化成了一个可复用模块。实际使用时aid和客户端ID可能需要根据具体设备和请求头动态调整但整体结构不变。5.2 自由字段的动态生成避免固定值带来的问题第一次用这段代码跑的时候我踩了一个小坑X-Apple-Client-Id如果每次固定不变连续多次登录后服务端会返回异常。这个字段大概率与客户端标识有关服务端会检测到同一“客户端”反复注册。解决方法是每次请求前重新生成一个随机ID时间戳加随机数即可。另一个动态字段是X-Apple-I-Client-Time。这个字段表示客户端本地时间服务端会比较校验。如果偏差超过几分钟接口可能直接拒绝请求或者在响应里返回“时间不同步”的错误码。使用UTC标准格式最稳妥比较逻辑是按秒精度做的。关于aid字段它和账号/设备绑定不同账号下这个值不同。建议从一个成功的抓包记录中提取默认值后续在自动化中从配置里读取而不是每次都写死同一个值。5.3 成功标准与异常响应解析登录接口返回的JSON结构相对清晰。成功时服务端会返回一个包含会话标识的字段例如某种token或session id同时会有一个statusCode字段为0。如果密码错误返回码和错误信息会明确指示方便自动化脚本记录失败原因。我在自动化里定义了一套判定逻辑statusCode 0且存在有效会话标识时视为登录成功其他情况记录错误码和消息进入失败处理流程。对于特定错误码比如验证码提示、风控拦截、频率限制不立即重试而是根据错误类型做等待或人工介入。5.4 集成到GitLab CI/Jenkins把登录模块变成测试前置步骤协议登录最常用的场景是在CI/CD流水线中作为前置步骤。以GitLab CI为例我之前把登录流程做成了一个独立的Python脚本在需要执行iOS自动化测试的Job中先调用登录模块拿到会话标识后写入环境变量再传给后续测试用例。before_script: - python login.py --account $TEST_ACCOUNT --password $TEST_PASSWORD session.json - export SESSION_ID$(python -c import json; print(json.load(open(session.json))[session_id])) test: script: - python run_ui_tests.py after_script: - python logout.py这个做法的好处是一个登录模块可以被多个Job复用账号密码统一放在CI变量中不再散落在测试代码里。Jenkins上思路一样可以用Credentials插件管理账号密码再用Pipeline的sh步骤调用Python脚本。6. 高频踩坑与稳定性优化这些坑我不希望你重踩6.1 模式混淆ECB还是CBC的判定陷阱我见到的逆向文章里最常翻车的就是模式判断错误。ECB模式密文分组独立CBC模式有IV且分组串联。判断时一定要用等长明文、修改单字节的实验方法。比如明文是12345678和12345679长度都是8字节只改最后一位。如果密文只有最后一组变化说明是ECB如果两组都变化就是CBC。我最初实验时把明文长度也改了结果密文分组分布完全不同差点误判成CBC浪费了半天时间。6.2 Key的编码问题hex vs ascii从二进制里提取的Key可能是两种形态之一直接以ASCII字符串形式存放在数据段或是以十六进制字符串表示实际字节。这两种形态在代码里完全不同。前者直接bxxx作为Key后者需要先bytes.fromhex()转换。动态Hook打印出来的最容易判断直接打印的hexdump会显示原始字节。如果Key是可见的ASCII字符你能在dump里直接读懂如果是乱码或不可见字符大概率是hex形式或原生二进制形式。这个细节决定了代码能不能跑通一不小心就废一片。6.3 响应中的加密字段与重放登录成功后服务端返回的JSON里可能还包含其他加密字段。许多人会误以为它们也要解密其实不必。我们关心的是会话标识和状态码服务端返回的其他字段通常由客户端在其他接口中使用和密码加密链路无关。重放请求也有讲究。同一个请求重放多次服务端可能因会话冲突或风控策略拒绝。所以自动化里不要简单循环同一请求每个账号登录一次后要生成新的客户端ID并等待几秒再操作。实测中连续两次相同请求的成功率大约只有60%左右加入随机延迟后显著改善。6.4 时间同步与失败重试策略X-Apple-I-Client-Time校验很严格。我在一台时间偏差超过5分钟的测试服务器上跑登录连续失败提示的超时错误信息里夹杂了“time”关键字才定位到问题。如果是容器环境尤其要注意基础镜像的时区设置最好在代码里用time.gmtime()生成UTC时间而不是依赖系统本地时间。重试策略上推荐退避重试第一次失败后等1秒第二次等5秒第三次等30秒连续失败5次直接报警。不要写死循环短时间重试否则很容易触发账号风控最后反而更难自动化。6.5 账号风控与频率限制最后一个坑也是自动化上线后最容易遇到的频率限制。协议登录虽然比UI自动化快但过于频繁地反复登录同一账号服务端会要求额外的验证步骤甚至锁定登录入口一段时间。我采取的策略是每个账号两次登录请求之间至少间隔30秒单账号每天登录次数不超过200次多账号任务错开时间不要在同一秒发起几十个请求。同时把所有错误码和响应时间记录下来建立监控一旦某账号连续失败自动暂停该账号的任务并告警。写在最后的实操心得如果你也准备做类似的事我的建议是先花半天时间把抓包环境彻底搞稳定证书信任、代理过滤、请求识别这三个环节不扎实后面全是空中楼阁。加密定位阶段动态Hook比纯静态分析快得多能Hook就优先Hook静态分析用来做交叉验证和补全上下文。最后分享一个小技巧拿到Key之后把所有原始凭据整理成一个独立的配置模块统一管理。无论是密钥、账号、还是常用请求头都放进去后续维护和交接都非常方便。不要像我一开始那样把密钥散写在多个脚本里等到需要统一换Key的时候你会回来感谢这条建议的。