【2023/7/26】云片滑块--逆向案例

逆向目标: 

行为验证-行为式验证码_智能区分人机验证码 -云片行为验证 (yunpian.com)https://www.yunpian.com/product/captcha

 

 抓包分析

        验证的流程分为两个步骤,第一个是请求验证码图片,第二步是验证,第一步的请求一共有五个参数,分别是cb、i、k、token、captchaId,其中token为上一次请求的返回得来的,captchaId一直不变,所以只需要把目光放到前三个加密的参数即可

 参数分析

        观察调用堆栈,发现调用堆栈很少,所以从堆栈入手比较快

         再最顶的堆栈打上断点刷新验证码,发现t的长度和cb很像,初步判断是cb

        我们再往上一步跟栈,cb的值由cbManager.preAdd()这个函数生成,是一个随机值,所以把js代码复制一下就行

  

           有一个关键的加密函数 this.encrypt(e),e是浏览器的一些指纹信息,直接把e给复制下来

encrypt加密函数在上面的位置,也是直接复制下来,分别加密了i和k,也就是我们请求的参数

        i的加密方式看起来像aes,k的话写明了用rsa,所以我们用crypto库就改写一下,或者扣代码的方式也行,这里我先写rsa加密,因为k的值为(e+n)再用rsa加密得到的,rsa的公钥也给出了

        两个库函数

const CryptoJS = require("crypto-js");
const JSEncrypt = require("node-jsencrypt");
function rsaEncrypt(t) {
    const publicKey = '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnOWe/gs033L/2/xR3oi6SLAMPBY5VledUqqH6dbCNOdrGX4xW+1x6NUfvmwpHRBA2C7xWDDvOIldTl0rMtERTDy9homrVqEcW6/TY+dSVFL3e2Yg2sVaehHv7FhmATkgfC2FcXt8Wvm99QpKRSrGKpcFYJwOj2F8hJh+rTG0IPQIDAQAB-----END PUBLIC KEY-----'
    var encrypt = new JSEncrypt();
    encrypt.setPrivateKey(publicKey);
    let data = encrypt.encrypt(t)
    return data;
}

         i的aes加密也用crypto库改写一下

CryptoJS.AES.encrypt(t, parse(e), {iv: parse(n)}).toString()

        到这里i和k的值就可以得到了

         再把captchaId和cb的值给处理一下返回就能得到完整的参数了,接下来使用curl转换工具转为python的requests请求

在python中使用execjs库去执行js加密文件即可

execjs.compile(open('./test.js', 'r', encoding='utf-8').read()).call('encrypt')

成功得到背景图片和滑块图片 !

 滑块验证

        随便滑动一下,得到验证接口,以及失败的返回结果,请求参数和之前请求的 图片的参数差不多,应该是同一套加密算法,主要要处理的就是轨迹问题以及如何找到缺口的位置。

        从调用堆栈找到加密的入口,和之前的请求图片大差不差,主要变化就是e的值变成了滑块的轨迹,可以看出轨迹被处理成了一个二维数组。         从堆栈往上找一下找到轨迹处理函数 

 主要部分在这里,先看i的生成是由reducePoints()函数完成

i = this.reducePoints()
r = (this.imgWidth - this.alertImgTag.width) * (this.offsetX / (this.imgWidth - 42)) / n;

this.jsonpRequest("/jsonp/captcha/verify", {
                    points: i,
                    distanceX: r
                })

reducePoints的函数,它的目的是减少一个位置数组(`this.position`)中的点的数量,使其不超过一个最大限制(`this.MAX_POINTS_AMOUNT`)。如果点的数量已经在这个限制之下,它会直接返回原始数组。否则,它会创建一个新数组,包含原始数组中的第一个点、最后一个点,以及在两者之间等间隔地选择的点。

1. `var t = D(this.position);` - 这行代码调用了函数 `D`,D函数是复制一下原来的位置数组,没什么意义

2. `if (t.length <= this.MAX_POINTS_AMOUNT) return t;` - 如果 `t` 的长度(即点的数量)已经不超过最大限制,那么直接返回原始数组。

3. `var e = [t[0]], n = t[t.length - 1], i = Math.floor(t.length / this.MAX_POINTS_AMOUNT);` - 初始化新数组 `e`,包含原始数组的第一个点。同时,将原始数组的最后一个点赋值给变量 `n`。然后计算出等间隔选择点所需的步长 `i`。

4. `if (i < 2) return t;` - 如果步长小于2(即原始数组中每个点都需要被包含在新数组中),那么直接返回原始数组。

5. `for (var r = 1; r < t.length - 2; r += i) e.push(t[r]);` - 使用步长 `i` 在原始数组中等间隔地选择点,并添加到新数组中。

6. `e.push(n), e;` - 将原始数组的最后一个点添加到新数组中,然后返回新数组。

因此,可以看出这个函数可以用于在保持数据分布特性的同时减少数据点的数量

key: "reducePoints",
    value: function() {
    var t = D(this.position);
    if (t.length <= this.MAX_POINTS_AMOUNT)
         return t;
    var e = [t[0]]
    , n = t[t.length - 1]
    , i = Math.floor(t.length / this.MAX_POINTS_AMOUNT);
    if (i < 2)
       return t;
    for (var r = 1; r < t.length - 2; r += i)
        e.push(t[r]);
    return e.push(n),e
            }

        接下来是轨迹生成,轨迹生成对于一般的滑块都有固定的生成方式,下面为轨迹代码的生成逻辑

        重点是i的赋值操作,他创建一个新数组,包含了当前坐标(取小数点后两位并转为整数)和从上次时间戳到现在的时间差。将这个新数组添加到 r.position 中,并调用 r.moveModule()。在这里打上断点,鼠标每移动一次都会断住,也可以通过抓包的方式改写js,每次进行console.log输出, 第一个是横坐标的x,第二个是纵坐标y,第三个是当前滑动的时间-开始滑动的时间。
下面讲一下轨迹生成的思路。

  1. 初始化轨迹列表locus,并计算目标图片在背景图片上的相对位置。
  2. 随机生成滑动起点的坐标(Slider_x,Slider_y)和起始时间(start_time),将它们添加到轨迹列表。
  3. 计算滑动的最大x坐标(max_x)和实际x坐标(real_x)。
  4. 随机生成一个范围值(range_),用于控制轨迹列表的长度。这里生成的较大的轨迹数组,每次移动的距离只加1左右,到时候还会把他切到50长度以内的数组。
  5. 通过循环生成轨迹列表,每次循环中:
    • 随机改变x坐标(Slider_x)和y坐标(Slider_y)。
    • 更新滑动时间(start_time)。
    • 将更新后的坐标和时间添加到轨迹列表。
    • 当滑动到达最大x坐标时,结束循环。
    • 对轨迹列表使用cut函数进行裁剪。
      def get_locus():
          det = ddddocr.DdddOcr(det=False, ocr=False, show_ad=False)
          with open('ft.png', 'rb') as f:
              target_bytes = f.read()
          with open('bg.jpg', 'rb') as f:
              background_bytes = f.read()
          res = det.slide_match(target_bytes, background_bytes, simple_target=True)
      
          locus = []
          x = int(res['target'][0] / 1.45)
          Slider_x = random.randint(865, 885)
          Slider_y = random.randint(1955, 1975)
          start_time = random.randint(120, 200)
          locus.append([Slider_x, Slider_y, start_time])
          max_x = Slider_x + x + random.randint(10, 30)
          real_x = Slider_x + x
      
          def cut(locus):
              len_locus = len(locus)
              if len_locus <= 50:
                  return locus
              start = [locus[0]]
              end = locus[-1]
              i = len_locus // 50
              if i < 2:
                  return locus
              for r in range(1, len_locus, i):
                  start.append(locus[r])
              start.append(end)
              return start
      
          range_ = random.randint(300, 451)
          print('预计数组长度:{}'.format(range_))
          for i in range(range_):
              x_random = random.randint(0, 1)
              start_time += 1
              if i % 10 == 0:
                  if random.randint(0, 1) == 1 and Slider_y < 1975:
                      Slider_y += 1
                  else:
                      Slider_y -= 1
                  if Slider_x >= max_x:
                  max_x = real_x
                  Slider_x = Slider_x - x_random
                  if Slider_x <= max_x:
                      locus.append([max_x, Slider_y, start_time])
                      print('实际数组长度:{}'.format(i))
                      break
              else:
                  Slider_x += x_random
      
              locus.append([Slider_x, Slider_y, start_time])
          locus = cut(locus)
          return x, locus

 通过python覆写之前的切割数组部分,然后将切割的数组扔进js加密部分

    def cut(locus):
        len_locus = len(locus)
        if len_locus <= 50:
            return locus
        start = [locus[0]]
        end = locus[-1]
        i = len_locus // 50
        if i < 2:
            return locus
        for r in range(1, len_locus, i):
            start.append(locus[r])
        start.append(end)
        return start

        构造一下js加密函数 

function locus_encrpt(offsetX, points) {
    distanceX = (304 - 60) * (offsetX / (304 - 42)) / 304;

    let t = {
        "points": points,
        "distanceX": distanceX,
        "fp": "e935061666a2bf07cf7f977b34f492ba",
        "address": "https://www.yunpian.com",
        "yp_riddler_id": "27eb1d66-a41a-4771-ac5c-3e5c6c08ab29"
    }

    t = JSON.stringify(t);
    var e = getRandomStr(16)
        , n = getRandomStr(16);

    return {
        cb: Math.random().toString(32).replace("0.", ""),
        i: CryptoJS.AES.encrypt(t, parse(e), {iv: parse(n)}).toString(),
        k: rsaEncrypt(e + n),
        captchaId: '974cd565f11545b6a5006d10dc324281'
    }
}

最后的结果

需要代码的可以到GitHub下载 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值