apple游客下单逆向分析

文章目录

  • [1. 写在前面](#1. 写在前面)
  • [2. 游客注册](#2. 游客注册)

【🏠作者主页】:吴秋霖
【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与开发工作!
【🌟作者推荐】:对爬虫领域以及JS逆向分析感兴趣的朋友可以关注《爬虫JS逆向实战》《深耕爬虫领域》
未来作者会持续更新所用到、学到、看到的技术知识!包括但不限于:各类验证码突防、爬虫APP与JS逆向分析、RPA自动化、分布式爬虫、Python领域等相关文章

作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者无关!如有侵权,请联系作者本人进行删除!

1. 写在前面

  在作者之前的文章中写过关于某果登录算法以及登录后的下单、查询流程及相关风控分析: Apple登录下单算法及流程逆向分析,除此之外还有另外一种方式(游客下单),无需账号登录直接注册激活一次性游客cookies然后用它去add bag商品绑定就可以直接下单

大致的流程如下所示:

商品链接页访问-> 游客CK是初始化 -> Beacon/DC -> Shield风控验证 -> 激活Cookie
加购物车 -> Echo埋点上报 -> 游客上报登录 -> 配送选择 -> 收货配置 -> 支付 -> 完成

2. 游客注册

apple的游客生成依赖于触发多个接口发包然后拿到服务端返回的Set-Cookie,核心细节就是CK中的部分字段初始化由算法生成,然后个别字段由算法计算提交接口下发,如下是整个游客CK中比较关键的一些字段信息,如下所示:

s_vi: 访客ID、JS外链可以触发也可以算法生成、保持链路一致
s_fid: 客户端ID、初始算法生成、保持链路一致
as_atb: add bag 后给出的token
shld_bt_ck: 两个shiled接口触发,解number数组提交授权
shld_bt_m: 风控链路参数、每一级的接口都需继承更新
as_ltn_us: 注册激活成功给的登录状态标识

init接口处请求的时候,需要携带一个fnode参数,这个参数它不是算法生成的,在商品首页访问的HTML中返回(唯一且动态),除此参数外还有一个as_sfaCK参数,在请求atb接口的时候如上一样前置商品首次访问的时候提取出来即可,如下所示:


结束initatb接口后就能拿到服务器端Set-Cookie信息,直接更新到Session会话中再去完成后续的接口触发,往下新增了一个s_fid字段参数,初始算法生成即可,如下所示:

python 复制代码
def generate_s_fid() -> str:
    return "-".join(
        "".join(secrets.choice("0123456789ABCDEF") for _ in range(16))
        for _ in range(2)
    )

再往下就来到了两次shld接口请求,第一次请求返回JSON,这就类型shld下发过来的题目(需要计算参数)如下所示:

通过对JSON数据的计算,得到第二次shld接口请求中的number参数,计算方式通过调用栈可以看到指向verify.js文件,代码是混淆过的,如下所示:

number就是在这里面计算的,解混淆后发现这个verify文件主要就是风控验证(包含了检测自动化环境),关于number的解法即给定了lowhighpartsresult,来求长度为parts 的数组,使其乘积等于result,且每个元素在[low, high] 内!解混淆后的核心源码如下所示:

javascript 复制代码
async function search(currentFactors, currentProduct, startFactor) {
  // 1. 超时检查
  if (isTimeout()) return null;

  // 2. 已找到 parts 个因数,且乘积等于 result
  if (BigInt(currentFactors.length) === targetLen) {
    if (currentProduct === targetProd) return [...currentFactors];
    return null;
  }

  // 3. 枚举 startFactor ~ maxIterations
  for (; startFactor <= maxIterations; startFactor++) {
    if (startFactor < minFactor) continue;
    if (currentProduct * BigInt(startFactor) > targetProd) continue;
    // 剪枝:若 result 不能被 (currentProduct * factor) 整除,且还没凑够 parts 个,则跳过
    if (targetProd % (currentProduct * BigInt(startFactor)) !== zero && ...) continue;

    currentFactors.push(startFactor);
    const result = await search(currentFactors, currentProduct * BigInt(startFactor), startFactor);
    if (result) return result;
    currentFactors.pop();
  }
  return null;
}
/**
   * 因数分解算法 - 找出满足条件的质因数组合
   */
  async function findFactorDecomposition(minFactor, maxIterations, targetLength, targetProduct, startTime, timeout) {
    function isTimeout() {
      const elapsed = new Date() - startTime;
      const limit = timeout || 15000;
      return elapsed > limit;
    }

    const targetLen = BigInt(targetLength);
    const targetProd = BigInt(targetProduct);
    const zero = BigInt(0);
    const one = BigInt(1);

    async function search(currentFactors, currentProduct, startFactor) {
      if (isTimeout()) return null;

      if (BigInt(currentFactors.length) === targetLen) {
        if (currentProduct === targetProd) return [...currentFactors];
        return null;
      }

      for (; startFactor <= maxIterations; startFactor++) {
        if (startFactor < minFactor) continue;
        if (currentProduct * BigInt(startFactor) > targetProd) continue;
        if (targetProd % (currentProduct * BigInt(startFactor)) !== zero && BigInt(currentFactors.length) + one < targetLen) continue;

        currentFactors.push(startFactor);

        const result = await search(currentFactors, currentProduct * BigInt(startFactor), startFactor);

        if (result) return result;

        currentFactors.pop();
      }

      return null;
    }

    return await search([], one, minFactor);
  }

  /**
   * 执行分解计算
   */
  async function runDecomposition(params) {
    const startTime = new Date();

    let factors = null;
    factors = await findFactorDecomposition(
      params?.low,
      params?.high,
      params?.parts,
      params?.result,
      startTime,
      params?.timeout
    );

    const took = new Date() - startTime;
    if (factors == null) factors = [];

    return {
      "number": factors,
      "took": took
    };
  }

Pow计算通过因数分解增加自动化成本,需在限定时间内完成!wdpatSttspatSkip相关的环境指纹用于区分浏览器类型与状况,还有chCToreqTo关联的超时标记暴露计算/请求超时,服务端可据此降权或拦截,numberDFS算法实现如下:

python 复制代码
def solve_number_array(low, high, parts, result, timeout_ms):
    def dfs(current, prod, start_val):
        if len(current) == parts:
            return current if prod == result else None
        for v in range(start_val, high + 1):
            if v < low or prod * v > result:
                continue
            if result % (prod * v) != 0 and len(current) + 1 < parts:
                continue
            current.append(v)
            res = dfs(current, prod * v, v)
            if res is not None:
                return res
            current.pop()
        return None

    nums = dfs([], 1, low) or []
    took_ms = int((time.time() - start) * 1000)
    return nums, took_ms

最终提交参数到二次Shield接口进行验证,拿到激活可用shld_ck参数,不然后续的流程基本都是541风控问题,参数如下所示:

python 复制代码
{
    "high": shld_data["high"],
    "low": shld_data["low"],
    "parts": shld_data["parts"],
    "result": shld_data["result"],
    "signature": shld_data["signature"],
    "X": shld_data["X"],
    "t": shld_data["t"],
    "timeout": shld_data["timeout"],
    "flagskv": {"patSkip": True, "chCTo": True}, 
    "number": number_list,
    "took": took_ms,
}

之后就是一系列的加车、attach流程了,还有一个Echo的埋点大JSON,可以尽量去模拟,如下所示:

固定也是可以的,但是在生产环境持续的对抗中是高危的行为,往往很多埋点风控就是通过某些字段进行实时离线分析再判研的!最终所有的调用接口如下(从游客注册到再下单验卡支付):

python 复制代码
1.  GET  product_link                    → 获取 fnode, as_sfa
2.  GET  /shop/beacon/atb
3.  GET  /shop/mdp/echo/echo.png
4.  GET  /shop/dc
5.  GET  /shop/favoritesx/fetch
6.  GET  /shop/sba/d/init?fnode=&product=
7.  GET  /shop/shld/work/v1/q?wd=0
8.  POST /shop/shld/work/v1/q?wd=0      → 提交 number 数组
9.  GET  /shop/updateSummary (×2)
10. GET  /shop/api/purchase-options
11. GET  /shop/api/applecare/data
12. GET  /shop/fulfillment-messages
13. GET  /shop/updateSEO
14. GET  product_link?add-to-cart=add-to-cart&atbtoken=...
15. GET  /shop/buy-iphone/iphone-17-pro?step=attach
16. GET  /shop/beacon/atb
17. GET  /shop/buyFlowAttachSummary/{PRODUCT}
18. GET  /shop/favoritesx/fetch
19. POST /shop/mdp/api/echo              → Echo 埋点
20. GET  /shop/bag                       → 获取 x-aos-stk
21. POST /shop/bagx?_a=calculate
22. POST /shop/bagx/checkout_now
23. GET  {api_domain}/shop/signIn?ssi=&up=t
24. POST {api_domain}/shop/signInx        → 游客登录
25. POST {api_domain}/shop/checkout/start
26. GET  {api_domain}/shop/checkout
27. POST {api_domain}/shop/checkoutx/fulfillment (×2)
28. POST {api_domain}/shop/addressx/ziplookup
29. POST {api_domain}/shop/checkoutx/shipping (×2)
30. POST {api_domain}/shop/checkoutx/billing (×N 礼品卡)
31. POST {api_domain}/shop/checkoutx/billing → continueFromBillingToReview

整个流程下来基本就是需要一点时间去分析API的加载流程,另一个就是解决获取shld_ck的那个number数组要求正确求解(在规定的timeout内提交),还有就是x-aos-stk会话认证参数,这个是不可以伪造的,跟前置参数提取一样通过checkout接口提取,最终实现如下:

2025年的班就上到这个时候了,明年在精进自己!提前预祝各位新年快乐~

相关推荐
退休倒计时5 分钟前
【每日一题】LeetCode 437. 路径总和 III TypeScript
算法·leetcode·typescript
老迟到的茉莉17 分钟前
Hermes 是谁?跟 Claude Code 差在哪
开发语言·python
学逆向的22 分钟前
汇编——内存
开发语言·汇编·算法·网络安全
tachibana227 分钟前
hot100 翻转二叉树(226)
java·数据结构·算法·leetcode
Day(AKA Elin)41 分钟前
【Day】MTP(Multi Token Prediction)技术学习
python·深度学习·学习·llama
生戎马1 小时前
高光谱拼接算法(七)USAC
算法
PhotonixBay2 小时前
共聚焦成像核心原理:针孔、PSF与三维形貌测量技术
人工智能·测试工具·算法
程序员小远2 小时前
接口测试之文件上传
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
江华森2 小时前
Python 实现高德地图找房(三):地图可视化与高德 JS API
开发语言·javascript·python
来一碗刘肉面2 小时前
C++中的引用语法
c++·算法