前言 最近一段时期,利用 AI 来辅助工作与学习变得越来越频繁,确切地说,使用 ChatGPT GPT-4o 模型的次数越来越多了。由此也出现了两个问题,一是 OpenAI 官方对于普通用户每日可用 GPT-4o 模型的次数有限。第二个问题也是最主要的问题,则是国内用户访问 ChatGPT 极受网络环境的影响,一般的代理根本没法访问,即使是优质的、自建的代理,在对话几次后,后续每对话一次都需要刷新一次网页来验证自身身份,使得在使用上用户体验感极差。
终于,以上限制我无法继续忍受,尤其是受网络环境所导致的极差用户体验。于是就找到了一款基于 ChatGPT 并集成了 GPT-3.5、GPT-4o mini 和 GPT-4o 模型的第三方 AI 应用,据此应用官网的介绍,其用户数量超过六百万,那功能和用户体验感想必自有保证。在使用过程中,发现该应用的会员订阅验证机制是基于第三方的应用内订阅平台,而非苹果官方平台。如此一来,该应用便被我顺利破解。虽然破解过程十分简单,按理说本不足为道,但鉴于许多应用采用的都是这种验证机制,都可基于类似的手段来一一破解,因此此应用的破解便颇具代表性,遂将这一过程记录成文。
免责声明 本文所有内容仅用于学术研究、技术探讨及教育学习的目的,旨在帮助读者加深对软件安全及逆向工程的理解。文中描述的技术与方法并非鼓励或支持任何未经授权的行为,亦不应将本文内容用于任何未经授权的目的。软件破解及相关技术的使用需遵守各国法律法规,任何个人或机构在实践时应对自身行为负责,作者不承担因不当使用本文内容所引发的任何责任。
订阅验证通信流程分析 在正式分析前,不妨先想想,当我们对应用的高级功能付完费完成订阅后,后续每次打开应用,应用是如何得知高级功能已经订阅了的?基于这个疑问,再作出进一步的思考,应用是否会在网络层面进行订阅状态的验证?以及具体是如何验证的?整个通信流程又是如何的?
有了猜想,于是我们就可以想到通过类似 Proxifier 这样的代理链工具,将此 AI 应用的全部 HTTP 流量强制转到 BurpSuite,以监控该应用在完整通信流程中所产生的全部 HTTP 流量。同时,在 BurpSuite 端开启请求与响应的劫持,后续可能需要篡改响应数据。
此时,再打开这个 AI 应用,回到 BurpSuite 中便可观察到如下 GET 请求,请求的域名为 api.revenuecat.com。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 GET /v1/subscribers/$RCAnonymousID%3A697911394ea640868b8a5d99e47f1266/offerings HTTP/1.1 Host: api.revenuecat.com X-Platform-Flavor: native User-Agent: *%20*/1046 CFNetwork/1404.0.5 Darwin/22.3.0 X-Revenuecat-Etag: 6145a6a0eaaba93a X-Client-Build-Version: 1046 X-Client-Bundle-Id: com.*.* X-Version: 4.41.2 X-Client-Version: 1.3 X-Platform-Version: Version 13.2.1 (Build 22D68) X-Platform: macOS X-Observer-Mode-Enabled: false Authorization: Bearer appl_gQpRDGQpplyrTBswHvTlWdSsPMU X-Storefront: USA Accept-Language: en-US,en;q=0.9 Accept: */* Content-Type: application/json X-Is-Sandbox: false X-Storekit2-Enabled: false Accept-Encoding: gzip, deflate, br Connection: keep-alive
用 Google 搜一下 RevenueCat,就能得知这其实是一个用于管理应用内订阅的移动 SDK 和 API, 为 Apple App Store 和 Google Play Store 复杂的订阅系统提供支持。也就是我在前面提到的第三方应用内订阅平台。
将如上面的请求放行,看看会收到什么响应。如下,响应状态码为 304 未修改,似乎与缓存有关。经过测试,如果这里不作处理,原封不动的将响应返回给 AI 应用程序,那后续将什么都不会发生。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 HTTP/1.1 304 Not Modified Date: Sun, 03 Nov 2024 07:19:20 GMT Connection: keep-alive server: envoy access-control-allow-origin: * access-control-expose-headers: X-Request-Id x-revenuecat-etag: 6145a6a0eaaba93a x-revenuecat-request-time: 1730618360809 x-signature: EIXmUGI5fWDSlDvDjpbSoJJiF3JsCZLtMZ/NUtwEat92TgAAqe6Ra7vFrglbnzvqC/uPP6D4697CxKTbwUAR+tQqVxKSm8/ZUCl88V/fBNjXgaMbGUuWtjI4WW5HLkzEgp1DBsTEBxHqURnNi8dW6PSHfzMTnvRkdTyU6jpjVUnPN8TKqAdfLJSd20mctOXP5n0QThXTrXZqlYZdd5VuQiaJowKbkyugWwQV+xLWyNeceUEL x-amzn-trace-id: Root=1-672723f8-11fa0d5f46ca89c4611ca65a x-envoy-upstream-service-time: 16 vary: Accept-Encoding x-request-id: badbd6c7-0439-447c-af24-1637931cbf08
所以,这里需要篡改删除响应中的 x-revenuecat-etag 标头(相当于清除缓存)并放行,使 AI 应用程序接收到该响应,这样才会有后续的请求发生。
不出所料,AI 应用程序发起了一个同样的请求,但却接收到了不一样的响应数据。仔细观察请求包中的 X-Revenuecat-Etag 标头,可推断出当这个 HTTP 标头的值为空时,才不会受缓存的影响,才能收到正常的响应数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 { "current_offering_id" : "default" , "offerings" : [ { "description" : "AI Plus standard offer" , "identifier" : "default" , "metadata" : null , "packages" : [ { "identifier" : "$rc_monthly" , "platform_product_identifier" : "aiplus_monthly" } , { "identifier" : "$rc_annual" , "platform_product_identifier" : "ai_plus_gpt_yearly" } , { "identifier" : "$rc_weekly" , "platform_product_identifier" : "ai_plus_chatgpt" } ] } ] , "placements" : { "fallback_offering_id" : "default" } }
进一步地,查阅 RevenueCat 官方 API 文档,得知/v1/subscribers/{app_user_id}/offerings 这个 API 接口是用于获取应用程序的报价信息。例如,如上响应中的 platform_product_identifier 字段表示商店中报价的标识符。根据字面意思来推测,ai_plus_chatgpt 可能代表按周订阅,aiplus_monthly 或许代表按月订阅,ai_plus_gpt_yearly 应该代表按年订阅。
继续回到 BurpSuite,观察后续的通信流量,发现应用程序又发起了一个新请求,且其中存在 X-Revenuecat-Etag 标头,尝试先放行请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 GET /v1/subscribers/$RCAnonymousID%3A697911394ea640868b8a5d99e47f1266 HTTP/1.1 Host: api.revenuecat.com X-Platform-Flavor: native User-Agent: *%20*/1046 CFNetwork/1404.0.5 Darwin/22.3.0 X-Revenuecat-Etag: 1db63f2915741c25 X-Client-Build-Version: 1046 X-Client-Bundle-Id: com.*.* X-Version: 4.41.2 X-Client-Version: 1.3 X-Platform-Version: Version 13.2.1 (Build 22D68) X-Observer-Mode-Enabled: false X-Platform: macOS Authorization: Bearer appl_gQpRDGQpplyrTBswHvTlWdSsPMU X-Storefront: USA Accept-Language: en-US,en;q=0.9 Accept: */* Content-Type: application/json X-Is-Sandbox: false X-Storekit2-Enabled: false Accept-Encoding: gzip, deflate, br Connection: keep-alive
随后,又收到了一个 304 状态码且带 x-revenuecat-etag 标头的空响应,依旧按照上面的处理方式,去除 x-revenuecat-etag 标头并放行,使 AI 应用程序接收到经过篡改的响应。
不出意料,AI 应用程序又发起了一个同样的请求,但响应有所不同了,见如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "request_date" : "2024-11-03T07:20:36Z" , "request_date_ms" : 1730618436398 , "subscriber" : { "entitlements" : { } , "first_seen" : "2024-11-01T08:32:51Z" , "last_seen" : "2024-11-03T06:04:46Z" , "management_url" : null , "non_subscriptions" : { } , "original_app_user_id" : "$RCAnonymousID:697911394ea640868b8a5d99e47f1266" , "original_application_version" : null , "original_purchase_date" : "2024-11-01T07:41:16Z" , "other_purchases" : { } , "subscriptions" : { } } }
放行此响应后,发现后续再无相关请求出现,这表明经过此请求与相应后,订阅验证流程已结束,如上几个请求与响应便是订阅验证的一个全流程。
中间人攻击客户端验证 经过了上面对订阅验证流程的流量分析,大概就能够推断出最后一个请求响应就是关键所在。
继续查阅 RevenueCat 官方 API 文档,见如下链接:
https://www.revenuecat.com/docs/api-v1#tag/customers/operation/subscribers
根据文档所述,/v1/subscribers/{app_user_id}接口是一个获取或创建客户的 API,作用是使用给定的 App User ID 获取客户的最新客户信息,如果不存在,则创建新客户。同时,官方还给出了一个响应示例,见如下 JSON。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 { "request_date" : "2019-07-26T17:40:10Z" , "request_date_ms" : 1564162810884 , "subscriber" : { "entitlements" : { "pro_cat" : { "expires_date" : null , "grace_period_expires_date" : null , "product_identifier" : "onetime" , "purchase_date" : "2019-04-05T21:52:45Z" } } , "first_seen" : "2019-02-21T00:08:41Z" , "management_url" : "https://apps.apple.com/account/subscriptions" , "non_subscriptions" : { } , "original_app_user_id" : "XXX-XXXXX-XXXXX-XX" , "original_application_version" : "1.0" , "original_purchase_date" : "2019-01-30T23:54:10Z" , "other_purchases" : { } , "subscriptions" : { "annual" : { "auto_resume_date" : null , "billing_issues_detected_at" : null , "expires_date" : "2019-08-14T21:07:40Z" , "grace_period_expires_date" : null , "is_sandbox" : true , "original_purchase_date" : "2019-02-21T00:42:05Z" , "ownership_type" : "PURCHASED" , "period_type" : "normal" , "purchase_date" : "2019-07-14T20:07:40Z" , "refunded_at" : null , "store" : "app_store" , "unsubscribe_detected_at" : "2019-07-17T22:48:38Z" } } } }
将官方提供的这份响应示例与我们的响应相对比,可发现最主要的差别在于后者缺失了 subscriber.entitlements 和 subscriber.subscriptions 字段内容。
参照官方提供的响应示例,将缺失的相关字段补充上,部分字段的值可参考请求 offerings 接口所获取的报价信息。然后重启应用以重来一遍订阅验证流程,直到接收到最后一个请求的响应时,劫持并篡改替换为如下内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 { "request_date" : "2024-11-03T07:20:36Z" , "request_date_ms" : 1730618436398 , "subscriber" : { "entitlements" : { "AI Plus" : { "product_identifier" : "ai_plus_gpt_yearly" , "purchase_date" : "2024-11-01T00:00:00Z" , "expires_date" : "2099-09-09T00:00:00Z" } } , "first_seen" : "2024-11-01T08:32:51Z" , "last_seen" : "2024-11-03T06:04:46Z" , "management_url" : null , "non_subscriptions" : { } , "original_app_user_id" : "$RCAnonymousID:697911394ea640868b8a5d99e47f1266" , "original_application_version" : null , "original_purchase_date" : "2024-11-01T07:41:16Z" , "other_purchases" : { } , "subscriptions" : { "ai_plus_gpt_yearly" : { "expires_date" : "2099-09-09T00:00:00Z" , "original_purchase_date" : "2024-11-01T00:00:00Z" , "purchase_date" : "2024-11-01T00:00:00Z" , "ownership_type" : "PURCHASED" , "store" : "app_store" } } } }
此刻,回到 AI 应用中,可发现应用中已显示“享受无限制的使用”,即表明破解成功。
总结下破解的两个关键。一是,当请求包中出现了 X-Revenuecat-Etag 标头值,需要去除,否则服务器端会认定客户端存在缓存,从而不返回任何响应数据。第二个关键,在服务器端返回客户的订阅信息到客户端之前,需要将此数据篡改为已订阅的数据,从而欺骗到位于客户端中的订阅验证机制。
iOS 端应用流量重写 以上破解针对的是 macOS 平台上的 AI 应用程序,而在 iOS 上恰好有此应用的同款应用,且 iOS 上的一些常见代理程序(如小火箭等)也都是支持 MITM 以及脚本重写流量的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 let obj = {};if (typeof $response == "undefined" ) { delete $request.headers ["X-RevenueCat-ETag" ]; obj.headers = $request.headers ; } else { let body = JSON .parse ( (typeof $response != "undefined" && $response.body ) || null ); if (body && body.subscriber ) { let subscriber = body.subscriber ; subscriber.entitlements ["AI Plus" ] = { product_identifier : "ai_plus_gpt_yearly" , expires_date : "2999-01-01T00:00:00Z" , purchase_date : "2024-11-01T00:00:00Z" , }; subscriber.subscriptions ["ai_plus_gpt_yearly" ] = { expires_date : "2999-01-01T00:00:00Z" , original_purchase_date : "2024-11-01T00:00:00Z" , purchase_date : "2024-11-01T00:00:00Z" , ownership_type : "PURCHASED" , store : "app_store" , }; obj.body = JSON .stringify (body); } } $done(obj);
那么,根据前面总结的破解的两个关键,编写如上 JavaScript 脚本,并将脚本导入至小火箭,配置如下两条规则,以及开启 HTTPS 解密。
现在,打开 iOS 端应用,即可发现应用被成功破解。
漏洞修复 经过相关查阅最终确认下来,这个问题的造成主要责任在于 RevenueCat,其在设计之初未优先考虑到安全风险,没有想到会受到 MITM 的攻击。好在 2023 年 7 月份,RevenueCat 对这一问题做出了解决。因此,对于此类攻击的防范,首先需要升级 SDK 至 4.25.0 版本及以上(iOS 平台)以启用 Trusted Entitlements 功能;其次还需正确地配置此功能的模式,因为至少就目前而言此功能即使启用了,但在默认配置下依旧是形同虚设,并不能真正防范中间人攻击,具体详细操作请参见下面这篇文章。
写在最后 原本,没有什么是花钱不能解决的,开头那两个问题,钱花够了自然就能轻松解决。但对于我这种不爱走寻常路的人来说,显然是不会选择常规手段来解决问题。倒不是本人抠抠搜搜不舍得花钱,遇到好用的东西和该支持的开源项目,我都是毫不吝啬地选择付费赞助支持的。只是我觉得比起常规手段,不如让人过过破解的瘾、享受享受 Hacking 的乐趣。
不过,虽破解了这款应用,但快乐也仅在破解成功后的那一瞬间,在未来我不会继续非法使用这款应用,主要是有两方面的考虑。一是如果长期利用破解手段来非法使用这款应用,所花的真金白银会由该应用所属的公司来承担,这对这家初创公司的伤害比较大。第二个原因则是在学习方面,AI 虽能帮助使用者快速获取“What”型问题的答案,但欲速则不达,对于深入学习、深层次理解“How”与“Why”型问题,仍需要系统性训练和自主思考,基础知识也特别重要。更何况,足够复杂的问题,AI 也无能为力,甚至还可能会出错。