极验三代滑块协议逆向

目标地址

aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vZGVtby9zbGlkZS1mbG9hdC5odG1s

请求流程

1、https://www.geetest.com/demo/gt/register-slide
注册验证码,第一次返回  challenge  和  gt

2、https://apiv6.geetest.com/gettype.php
获取资源路径,此接口可以忽略

3、https://apiv6.geetest.com/get.php
此接口需要传入第一个接口中的challenge、gt和w,w一共需要构造3次,该请求是第一次。返回了c和s

4、https://apiv6.geetest.com/ajax.php
此接口也需要传入第一个接口中的challenge、gt和w,该请求是第二次构造w参数。

5、https://apiv6.geetest.com/get.php
此地址为第二次请求,在第3步的时候,请求了第一次,该接口返回了验证码图片的地址,以及新的challenge和s值,该challenge为原来旧的challenge加上两位新的字符串。值得注意的是,该验证码图片顺序被打乱需要裁切还原。

6、https://apiv6.geetest.com/ajax.php
第二次请求该地址,并且也是最终的验证方法。需要传入gt、challenge和w,返回{"validate": "202cb962ac59075b964b07152d234b70"}形式,即代表成功。

难点

一、第一次构造w

搜索关键字\u0077,即可看到参数w的生成逻辑,w = i + r。

跟栈进入 t[$_CEEJl(1169)] ,发现r实际上等于new X()[$_CGCJs(17)](this[$_CGCI_(1119)](e));扣出Xthis[$_CGCI_(1119)](e)即可

this[$_CGCI_(1119)](e) 为16位字符串,由e方法连续执行4次拼接而成

X直接整个扣出即可

来到下一步,开始处理参数o,$_BF_()[$_CEEIh(1184)] 为AES加密,初始向量是 0000000000000000,扣代码或者用库都行,he[$_CEEIh(491)]为JSON.stringify()方法,不用扣,t[$_CEEIh(1119)]() 为rsa加密需要用到的key,须与上一步r中得到的16为字符串保持一致。

唯一需要注意的是,这里的 t[$_CEEJl(363)] 为Config对象,注意该对象中, challenge  和  gt 需要使用第一个接口中返回的,其他参数固定即可。

最后一个i参数,直接将p对象全部抠出来即可

至此,第一个w参数,已经全部逆向完毕。主要代码如下:

function Aes_encrypt(text, key_value) {
    var key = CryptoJS.enc.Utf8.parse(key_value);
    var iv = CryptoJS.enc.Utf8.parse("0000000000000000");
    var srcs = CryptoJS.enc.Utf8.parse(text);
    var encrypted = CryptoJS.AES.encrypt(srcs, key, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    for (var r = encrypted, o = r.ciphertext.words, i = r.ciphertext.sigBytes, s = [], a = 0; a < i; a++) {
        var c = o[a >>> 2] >>> 24 - a % 4 * 8 & 255;
        s.push(c);
    }
    return s;
}



function first_w(register_info){
    config = {
        gt: register_info["gt"],
        challenge: register_info["challenge"],
        offline: false,
        new_captcha: true,
        product: "float",
        width: "300px",
        https: true,
        api_server: "apiv6.geetest.com",
        protocol: "https://",
        type:"fullpage",
        aspect_radio: {slide: 103, click: 128, voice: 128, beeline: 50},
        beeline: "/static/js/beeline.1.0.1.js",
        cc: 20,
        click: "/static/js/click.3.1.0.js",
        fullpage: "/static/js/fullpage.9.1.9-crdubp.js",
        geetest: "/static/js/geetest.6.0.9.js",
        i:"-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1",
        slide: "/static/js/slide.7.9.2.js",
        static_servers: ['static.geetest.com/', 'static.geevisit.com/'],
        voice: "/static/js/voice.1.2.4.js",
        ww: true
    }
    // 根据aeskey 生成256位字符串
    var r = new X()['encrypt'](register_info["aeskey"]); 

    // AES加密 生成944位数组
    o = Aes_encrypt(JSON['stringify'](config), register_info["aeskey"])
    i = p['$_HEz'](o)

    // w长度固定 1516
    w = i + r
    return w
}

二、第二次构造w

第二个w,与另外两个略有不同,无法通过字符串直接找到,所以直接在栈里面找到如下记录:

即可找到参数w的生成逻辑,这里的代码比较绕,建议拿出来,格式化后再看。t[$_CFCJJ(1147)]为最终的w值,t[$_CFCIL(1160)] 为w的生成规则,跟栈进入。

首先,需要找到这个对象,该对象中的passtime比较重要,rp的加密过程中,也需要用到passtime,两者必须保持一致,rp为gt+challenge+passtime的MD5加密,即下图中的H方法。

解决完该参数后,继续往下,即可看到 t[$_CFCJJ(1147)] 的完整逻辑,p对象和,在构造第一个w的时候,已经扣过了,可以直接用;c[$_CFFII(17)]为AES加密,也可以直接用;i[$_CFFJL(1119)]()为16位随机字符串,继续用第一步生成的那个原字符串。

值得注意的是,在构造以上r对象的时候,以下[$_CFEEQ(1053)][$_CFEEQ(1003)]方法,仅在第一次执行时,能获取到正确结果。

至此,第二个w参数也构造完毕,主要代码如下所示:

function second_w(register_info){
    // 这里的aeskey与第一次的aeskey  需要保持一致
    r = {
        "lang": "zh-cn",
        "type": "fullpage",
        // 省略部分参数...请自行补齐
        "tt": "M3d8Pjp8Pjp8Pjp7.*M/SQe5e,85((n(5((((bn,5nbe,9(((.9(((()-1-)3,(@M9-U-)3)(?/)(NMM3)M?MM-N1?-U1)ME2@MRM91d-N6:5?EU-)61-*MM-N3)M?N9-j/*N1-jI)1E/,/)(R3)):JE9c:OJ4RH2cCeOQ5V.:MPDGb-h(B1,5b8q5b,5,bb(e5,qn(b((eb(((eV*(E/(/)M)NU(9/)M5-)M9,)(E1(/)()OU(9-19",
          },
          "dnf": "dnf",
          "by": 0
        },
        "passtime": 2077,
        "rp": CryptoJS.MD5(register_info["gt"] + register_info["challenge"] + 2077).toString(),
        "captcha_token": "1407818801",
        "pwsu": "czuubzkl"
    }
    // console.log(r);
    w = p['$_HEz'](Aes_encrypt(JSON.stringify(r), register_info["aeskey"]))
    return w
}

三、第三次构造w

第三个w参数,与前两个基本大同小异。
首先,仍然通过关键字搜索,定位到以下位置,得到
u = r[$_CAHJS(737)]()
l = V[$_CAHJS(392)](gt[$_CAIAK(254)](o), r[$_CAIAK(744)]())
h = m[$_CAIAK(792)](l)
w = h+u
的基本逻辑

u = r[$_CAHJS(737)](),与构造第一个w时的方式相同,不多赘述。
l = V[$_CAHJS(392)](gt[$_CAIAK(254)](o), r[$_CAIAK(744)]()),这一步构造o对象时,需要注意其中的userresponse、passtime、rp、aa四个参数
先找到o的定义位置,即可知道o对象各个参数的生成规则,
userresponse: H(滑块距离, new_challenge),
passtime: 滑块通过时间
aa: e,

e由外部传入,跟栈进入,这里生成的l,即为上一步需要的e。
仔细观察,可以发现,n[$_CJJJU(67)][$_CJJJU(1033)]n[$_DAAAU(67)][$_CJJJU(345)]分别为第五步接口中获取到的c和s,不多赘述。着重看下n[$_DAAAU(985)][$_CJJJU(1075)]n[$_CJJJU(985)][$_CJJJU(1073)]两个方法。这两个方法,前者为加密方法,直接将n对象整体抠出来。

后者稍微复杂一点,跟栈进入后,可以发现,该方法对轨迹进行加密,需要改造该方法为接收外部传入的轨迹。

改造后,如下图所示,在调用时传入轨迹即可得到结果

到这里,只剩下最后的h参数,跟前面一样的操作,直接将整个对象扣下来即可

最后拼接h+u参数,即完成了第三个w的参数的构造。需要注意的是,在构造第三个参数的o对象的时候,轨迹最后一组的z值要与o的passtime相同,轨迹最后一组的x值,要与滑动距离相同。

最后,放上需要用到的一些工具代码

import requests, time, random, json, execjs, base64
from PIL import Image
import io
import ddddocr, cv2


class Captcha:
    #还原乱序底图
    def ImgRestore(self):
            # 打开PNG图片
        image = Image.open("极验/三代2.0/pic/乱序.webp")
        # 创建新的空白图片
        new_image = Image.new('RGBA', (260, 160))
        # 坐标
        k = 0
        # 定值, 在极验3.0版本中, 通过画布断点获取数据
        ut = [39, 38, 48, 49, 41, 40, 46, 47, 35, 34, 50, 51, 33, 32, 28, 29, 27, 26, 36, 37, 31, 30, 44, 45, 43, 42,
            12, 13, 23, 22, 14, 15, 21, 20, 8, 9, 25, 24, 6, 7, 3, 2, 0, 1, 11, 10, 4, 5, 19, 18, 16, 17]
        # ut有多少参数, 就有多少个小碎片拼图
        for _ in range(len(ut)):
            # 通过计算得出 小碎片的起始坐标
            c = ut[_] % 26 * 12 + 1
            # 计算是否当前小碎片在第一层还是第二层, 三元表达式
            u = (lambda: 80 if 25 < ut[_] else 0)()
            # 将裁切下来小碎片的数据存储
            image2 = image.crop((c, u, c + 10, u + 80))
            # 当存储量达到26, 则重置小碎片的放置坐标, 每层只能放26小碎片
            if _ == 26:
                k = 0
            if _ > 25:
                # 第二层
                new_image.paste(image2, (k, 80))
                k += 10
            else:
                # 第一层
                new_image.paste(image2, (k, 0))
                k += 10
                
                
        # 保存最终图片  随机名称
        new_image.save("极验/三代2.0/pic/顺序.png")    
        buffered = io.BytesIO()
        # 将数据写入buffered里
        new_image.save(buffered, format="PNG")
        return {'base64': base64.b64encode(buffered.getvalue()).decode()}

    # 保存乱序验证码底图
    def downloadImage(self, bgPath, slicePath, session):
        path1 = bgPath.replace("jpg", "webp") #webp为乱序底图
        url1 = "https://static.geetest.com/"+path1
        response1 = session.get(url1)
        filename1 = '极验/三代2.0/pic/乱序.webp'  
        # 使用'wb'模式打开文件用于写入二进制数据  
        with open(filename1, 'wb') as f:  
            # 将请求返回的内容写入文件  
            f.write(response1.content)
            f.close()
            
        url2 = "https://static.geetest.com/"+slicePath#png为滑块图片
        response2 = session.get(url2)
        filename2 = '极验/三代2.0/pic/滑块.png'  
        # 使用'wb'模式打开文件用于写入二进制数据  
        with open(filename2, 'wb') as f:  
            # 将请求返回的内容写入文件  
            f.write(response2.content)
            f.close()
        
    # 识别距离
    def getDIstance(self):
        with open("极验/三代2.0/pic/顺序.png", "rb") as f:
            bg_content = f.read()
        
        with open("极验/三代/pic/滑块.png", "rb") as f:
            slider_content = f.read()
        
        slider = ddddocr.DdddOcr(det=False, ocr=False, show_ad=False)
        ret = slider.slide_match(slider_content, bg_content, simple_target=True)
        target = ret["target"]
        return target[0]

    
    def random_x(self, count, max):
        res_arr = [random.randint(1, max) for _ in range(count)]
        res_arr.sort()
        return res_arr
        
    def random_y(self, count, max):
        res_arr = [random.randint(0, max) * -1 for _ in range(count)]
        res_arr.sort(reverse=True)
        return res_arr

    def random_z(self, count, max):
        res_arr = set()
        while len(res_arr) < count:  
            # 生成一个随机数并添加到集合中,集合会自动去重  
            res_arr.add(random.randint(0, max - 1))  
            
        # 将集合转换为列表,因为集合是无序的,需要排序  
        sorted_res_arr = sorted(res_arr)  
        return sorted_res_arr  

    def gen_track(self, distance, time):
        # 轨迹的前两项和最后一项  需要手动调整
        track = [
            [random.randint(-50, -10), random.randint(-50, -10), 0],
            [0, 0, 0]
        ]
        count = int(1.46 * dis + 8.36)
        xs = self.random_x(count, distance)
        ys = self.random_y(count, random.randint(5, 15))
        zs = self.random_z(count, time)

        res = [list(t) for t in zip(xs, ys, zs)]
        track += res
        track.append([distance, track[-1][1], track[-1][2] + random.randint(100, 200)])
        print("轨迹长度-->",len(track))
        return track

至此,整个过程全部结束。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值