【JavaScript 逆向】极验三代滑块验证码逆向分析

声明

本文章中所有内容仅供学习交流,相关链接做了脱敏处理,若有侵权,请联系我立即删除!

案例目标

极验验证码 demo:aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vZGVtby8=

滑动验证码:aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vZGVtby9zbGlkZS1mbG9hdC5odG1s

文件版本:slide.7.8.9.js

以上均做了脱敏处理,Base64 编码及解码方式:

import base64
# 编码
# result = base64.b64encode('待编码字符串'.encode('utf-8'))
# 解码
result = base64.b64decode('待解码字符串'.encode('utf-8'))
print(result)

案例分析

抓包

刚进入页面,F12 打开开发者人员工具,未点击生成验证时 Network 中的抓包情况:

现对关键部分进行分析:

  1. register-slide 注册滑动条请求,响应返回:

    • challenge: "d4e44298ed09f654b3c284a5fb6d72ad",动态变化,关键参数

    • gt: "019924a82c70bb123aae90d483087f94",固定值

  2. gettype.php 获取验证码,Query String Parameters:GET 请求时,参数以 url string 的形式进行传递提交了 gt、callback:

    • gt:register-slide 时响应返回的定值

  3. get.php 以 url string 的形式传递了一些参数:

    • gt: register-slide 时响应返回的定值

    • challenge: register-slide 时响应返回的定值

    • w:加密了,环境校验,轨迹

点击滑块后,响应返回的新数据接口:

  1. ajax.php 以 url string 的形式传递了一些参数:

    • gt: register-slide 时响应返回的定值

    • challenge: register-slide 时响应返回的定值

    • w:加密了,环境校验,轨迹,跟前文 get.php 中的 w 值不一样

  2. get.php 以 url string 的形式传递了一些参数:

    • gt: register-slide 时响应返回的定值

    • challenge: register-slide 时响应返回的定值

    • Preview 响应预览中 bg: 带缺口的背景图(乱码),3e72d088a.jpg:

    • fullbg:完整的背景原图(乱码),cd0bbb6fe.jpg:

    • slice:滑块图片,3e72d088a.png

  3. ajax.php 以 url string 的形式传递了一些参数为:

    • gt: register-slide 时响应返回的定值

    • challenge: get.php 时响应返回

    • w:加密了,环境校验,轨迹,跟前文 ajax.php 中的 w 值不一样,至此三个 w 值均不一样

    • 响应预览中,成功返回,message: "success",success: 1,validate 值:​​​​​​

    • 响应预览中,失败返回 message: "fail",success: 0:

抓包步骤梳理分析: 

  1. 进入页面,服务器响应返回一些参数(challenge、gt 等)和一些关键的 js 文件,用于生成图片及校验;

  2. 点击按钮进行验证后,会调用到第一步中的一些 js 文件,生成缺口图、完整背景图、滑块图、w 值等;

  3. 校验滑块是否对其缺口、轨迹是否异常、相关参数是否正确等,判断验证是否成功。

逆向调试

底图还原

点击按钮进行验证,会弹出滑动验证码,审查元素可以发现,底图是通过 canvas 绘制出来的: 

所以可以直接通过事件断点进行定位:

Sources → Event Listener Breakpoints → Canvas → Create canvas context,然后点击刷新验证码即会在 slide.7.8.8.js 文件处断住,点击左下角 { } 进行格式化操作,断在了第 295 行:

控制台打印输出一下该行内容:

// canvas.getContext(contextID)
var o = canvas.getContext('2d');

参数 contextID 指定想要在画布上绘制的类型, '2d' 指定了二维绘图,返回 CanvasRenderingContext2D 对象,该接口是 Canvas API 的一部分,可为 <canvas> 元素的绘图表面提供 2D 渲染上下文,它用于绘制形状,文本,图像和其他对象。

i 为 canvas 画布,宽为 312,长为 160,对应乱序背景图片的长宽:

正常验证码背景图片的长为 160,宽为 260:

该函数体中的内容经过控制流平坦化混淆处理,打乱了函数原有代码执行流程及函数调用关系,使代码逻变得混乱无序,大体架构为:

for(x){
    switch($_DAHHo){
        case xx:
        ...
        break;
        case xxx:
        ...
        break;
    }
}

更多相关可参考:【JavaScript 逆向】AST 技术反混淆

大致为从大数组中根据指定的逻辑按照下标进行取值操作:

在第 299 行打下断点,控制台打印输出:

CanvasRenderingContext2D.drawImage() 方法提供了多种在画布(Canvas)上绘制图像的方式,此处为画图操作:

drawImage(image, dx, dy)
// image:绘制到上下文的元素
// dx:image 的左上角在目标画布上 X 轴坐标
// dy:image 的左上角在目标画布上 Y 轴坐标

img 的 src 末尾为 7bfaaa72b.webp,就是乱序的完整背景图片:

下一行又绘制了一个 2d 画布:

直接在第 312 行打下断点,第 304、305 行对应了上文验证码背景图的长宽:

其中有个 for 循环需要分析一下:

for (var a = r / 2, _ = 0; _ < 52; _ += 1) {
    var c = Ut[_] % 26 * 12 + 1
      , u = 25 < Ut[_] ? a : 0
      , l = o[$_CJEZ(30)](c, u, 10, a);
    s[$_CJEZ(84)](l, _ % 26 * 10, 25 < _ ? a : 0);
}

前文提到完整乱序背景图的比例为 312 x 160,即宽为 320,长为 160,a = r / 2,r 为图片长 ,即 a = 80,为图片长度的一半,此处将图片横切分割为了上下两等份,Ut[_] % 26 * 12 + 1,_ 值为 52,再将图片上下两部分纵向切割为了 26 等份,Ut 数组为取下标的顺序,即数组还原顺序,为固定的,% 26 * 12 + 1 为特征码,不会变动,25 < Ut[_] ? a : 0 判断图片是上半部分还是下半部分,正确图片的顺序为:

0: 39   10: 50  20: 31  30: 14  40: 3   50: 16
1: 38   11: 51  21: 30  31: 15  41: 2   51: 17
2: 48   12: 33  22: 44  32: 21  42: 0
3: 49   13: 32  23: 45  33: 20  43: 1
4: 41   14: 28  24: 43  34: 8   44: 11
5: 40   15: 29  25: 42  35: 9   45: 10
6: 46   16: 27  26: 12  36: 25  46: 4
7: 47   17: 26  27: 13  37: 24  47: 5
8: 35   18: 36  28: 23  38: 6   48: 19
9: 34   19: 37  29: 22  39: 7   49: 18
l = o[$_CJEZ(30)](c, u, 10, a);

CanvasRenderingContext2D.getImageData(),返回一个 ImageData 对象,用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为 (sx, sy)、宽为 sw、高为 sh:

ImageData ctx.getImageData(sx, sy, sw, sh);
// sx:将要被提取的图像数据矩形区域的左上角 x 坐标
// sy:将要被提取的图像数据矩形区域的左上角 y 坐标
// sw:将要被提取的图像数据矩形区域的宽度
// sh:将要被提取的图像数据矩形区域的高度
s[$_CJEZ(84)](l, _ % 26 * 10, 25 < _ ? a : 0);

CanvasRenderingContext2D.putImageData() 是 Canvas 2D API 将数据从已有的 ImageData 对象绘制到位图的方法,如果提供了一个绘制过的矩形,则只绘制该矩形的像素:

void ctx.putImageData(imagedata, dx, dy);
// ImageData:包含像素值的数组对象
// dx:源图像数据在目标画布中的位置偏移量(x 轴方向的偏移量)
// dy:源图像数据在目标画布中的位置偏移量(y 轴方向的偏移量)

这里就是同时对上下两部分进行拼凑,_ % 26 * 10 表示每个小块取 10 px 像素。

随便拖动滑块,失败后 Network 中会抓包到如下内容:

响应预览中返回失败信息,ajax.php 以 url string 的形式传递的参数中包括 w 参数,通过 Initiator 跟栈进去: 

在第 4567 行 return 处打下断点,再次拖动滑块即会断住:

向上跟栈到第 1182 行,s 中包含请求 url 的全部信息:

所以要找到 s 生成的位置,接着向上跟栈,跟到函数 k 处,在第 1128 行 s 已经被作为参数传进来值了:

接着向上跟,跟到第 868 行,n 即 s 的值:

往上找,在第 844 行打下断点,o 中包含我们所要的 w 参数:

进一步往上跟栈到 $_CCBd 处,在第 6076 行,可以看到 w 参数的值由 h + u 生成:

所以接下来需要知道 h 和 u 的值是怎么生成的,c 的生成位置在第 6065 行,h 的生成位置在第 6067 行:

var u = r[$_CAHJR(706)]()
  , l = V[$_CAIAZ(339)](pt[$_CAHJR(278)](o), r[$_CAHJR(721)]())
  , h = m[$_CAIAZ(769)](l)

u 参数逆向分析

u 由 r[$_CAHJR(706)] 方法生成,先在第 6065 行打下断点,断住后,选中跟进过去,跳转到第 6206 行,在第 6215 行打下断点,点击下一个断点即会断住:

控制台打印后可以看到,返回值 e 为 u 参数的值,定义在第 6212 行,$_CBFJf(339) 为 "encrypt",同时 new U() 的原型链中包含 setPublic 方法,设置公钥,因此可以推测,这里是经过了 RSA 加密,this[$_CBFJf(721)](t) 为一串明文值,先跟进到 this[$_CBFJf(721)] 中,会跳转到第 6196 行,在下一行打下断点,Mt 即为那串明文值:

Mt 是由 rt 方法赋值的,跟进到 rt 方法中,在第 4197 行,在第 4203 行打下断点,可以知道 rt 方法的返回值是由四个 t 方法的值加起来得到的,长度为 16 位:

进一步跟进到 t 方法中,在第 4187 行,在 4192 行打下断点,返回值即为 t 方法的值,每次生成结果不一样:

(65536 * (1 + Math[$_BFBFy(14)]()) | 0)[$_BFBFy(287)](16)[$_BFBEu(489)](1);

控制台打印一下这部分内容:

根据打印内容,手动解混淆:

(65536 * (1 + Math["random"]()) | 0)["toString"](16)["substring"](1);

所以逻辑已经清楚了,直接通过代码对 this[$_CBFJf(721)](t) 的值进行复现即可:

function randomStr() {
    var data = "";
    for (var index = 0; index < 4; index++) {
        data += (65536 * (1 + Math["random"]()) | 0)["toString"](16)["substring"](1);
    }
    return data;
}
​
// console.log(randomStr());
// console.log(randomStr().length);

明文部分解决了,再来看看 RSA 加密部分,跟进到 new U() 原型链中 setPublic 设置公钥的位置:

跳转到第 2895 行,在第 2896 行打下断点,t 为 key 值,e 为公钥模数:

所以接下来只需要把 U 定义的位置找到,导出为全局变量调用即可拿到 key 值,ctrl + f 局部搜索 var U =,会发现其定义位置在第 2030 行,在后面加个 window.yyy = U; 即可导出,整个 js 文件扣下来改写后如下:

!function(){
    wv_ZX.$_AA = function() {...
    }();
    wv_ZX.$_Bo = function() {...
    }();
    wv_ZX.$_CN = function() {...
    }();
    wv_ZX.$_Dg = function() {...
    }();
    function wv_ZX() {}
    !function(){
        ...
        var U = function(){
            ...
        }();
        window.yyy = U;
    }();
}();

粘贴到 Snippets 中,打印结果无误:

key 处 debugger 后,t 值与 e 值也与网页 js 中的一致:

接下来就可以复现 u 值了:

!function(){
    wv_ZX.$_AA = function() {...
    }();
    wv_ZX.$_Bo = function() {...
    }();
    wv_ZX.$_CN = function() {...
    }();
    wv_ZX.$_Dg = function() {...
    }();
    function wv_ZX() {}
    !function(){
        ...
        var U = function(){
            ...
        }();
        window.yyy = U;
    }();
}();
function randomStr() {
    var data = "";
    for (var index = 0; index < 4; index++) {
        data += (65536 * (1 + Math["random"]()) | 0)["toString"](16)["substring"](1);
    }
    return data;
}
​
function getU(){
    return new window.yyy()['encrypt'](randomStr());
}
​
// console.log(getU());

成功获取到 u 值:

l 参数逆向分析

u 值获取到了,下一步就需要获取 l 值了,l 是个大数组:

控制台打印下各部分内容,这里也是个加密,pt[$_CAHJR(278)](o) 包含很多 JSON 格式的信息:

还原一下,r[$_CAHJR(721)]() 就是随机的是十六位字符串:

V["encrypt"](pt["stringify"](o), randomStr())

stringify 是将 JSON 格式转换为字符串,所以那部分信息是由 o 生成的,o 定义在第 6000 行:

拖动几次滑块对比一下参数 o 中有哪些值会发生变化:

imgload 为图片生成时间,不一样的有 aa、passtime、rp、userresponse,滑块拖动的距离不一样,aa 值的长度会不一样,所以 aa 的值很可能是滑块的移动轨迹,aa 定义在第 6005 行:

e 值是函数传进来的参数,所以向上跟栈查看生成的位置,在第 8154 行:

l = n[$_CJJJ_(957)][$_DAAAl(1022)](n[$_DAAAl(957)][$_DAAAl(1027)](), n[$_DAAAl(90)][$_CJJJ_(1004)], n[$_CJJJ_(90)][$_CJJJ_(386)]);

打下断点后,控制台打印一下各部分内容:

n[$_CJJJ_(957)][$_DAAAl(1022)] 为一个函数,传入了三个参数,n[$_DAAAl(957)][$_DAAAl(1027)]() 的值为 aa 的一部分,为轨迹值,n[$_DAAAl(90)][$_CJJJ_(1004)] 为数组,值是固定的, n[$_CJJJ_(90)][$_CJJJ_(386)] 是一个八位字符串,值会变化,根据控制台打印的内容可解混淆为:n["$_CJT"]["c"]n["$_CJT"]["s"],这两个值是 get.php 响应返回的:

因此接下来就需要跟进到 n[$_DAAAl(957)][$_DAAAl(1027)] 中,从第 4049 行一直到第 4117 行,在第 4092 行打下断点,t 即为鼠标轨迹,为 x 轴,y 轴,时间,自执行函数括号中的 this[$_BEGJj(311)] 为传入的轨迹值:

在第 4117 行打下断点,控制台打印,为 n[$_DAAAl(957)][$_DAAAl(1027)]() 的值:

对其内容逐个分析:

r[$_BEHAq(439)]($_BEHAq(50)) + $_BEGJj(476) + i[$_BEGJj(439)]($_BEGJj(50)) + $_BEHAq(476) + o[$_BEHAq(439)]($_BEHAq(50));

控制台打印还原:

r["join"]("") + "!!" + i["join"]("") + "!!" + o["join"]("");

r 在第 4093 行定义为一个空数组,传入的鼠标轨迹:

  • r["join"](""):x轴

  • i["join"](""):y轴

  • o["join"](""):时间

同样将这一部分内容定义为全局变量导出,加到 W[$_CJEZ(251)] 的最后:

window.get_track = W[$_CJEZ(251)]["\u0024\u005f\u0047\u0046\u004a"];

将鼠标轨迹作为参数传递进去,mouseTrack:

"\u0024\u005f\u0047\u0046\u004a": function(mouseTrack) {
    var $_BEGJj = wv_ZX.$_CN
      , $_BEGIl = ['$_BEHCf'].concat($_BEGJj)
      , $_BEHAq = $_BEGIl[1];
    $_BEGIl.shift();
    var $_BEHBo = $_BEGIl[0];
    function n(t) {...
    }
    var t = function(t) {...
    }(mouseTrack)
      , r = []
      , i = []
      , o = [];
    return new ct(t)[$_BEHAq(94)](function(t) {...
    }),
    r[$_BEHAq(439)]($_BEHAq(50)) + $_BEGJj(476) + i[$_BEGJj(439)]($_BEGJj(50)) + $_BEHAq(476) + o[$_BEHAq(439)]($_BEHAq(50));
}

获取轨迹值,this[$_BEGJj(311)]

传入导出的 window.get_track() 中,成功得到想要的结果:

至此传入的三个参数都解决了,就需要跟进到 n[$_CJJJ_(957)][$_DAAAl(1022)] 函数中没在第 4135 行:

同样将这部分内容导出即可:

window.get_func = W[$_CJEZ(251)]["\u0024\u005f\u0042\u0042\u0045\u0053"];

将这一部分封装成函数,参数值先固定,验证一下:

function getTrack_(){
    return window.get_func(window.get_track([[-41,-33,0],[0,0,0],[1,0,67],[5,0,84],[10,0,88],[17,0,96],[24,0,104],[29,0,111],[33,0,117],[36,0,128],[39,0,133],[40,0,144],[42,0,148],[43,-1,155],[44,-1,164],[46,-1,171],[48,-2,177],[49,-2,186],[50,-2,194],[51,-2,207],[51,-2,254]]), [12, 58, 98, 36, 43, 95, 62, 15, 12], "705a5874");        
}

成功获取到结果,aa 参数复现完成:

对比一致: 

接下来是 userresponse,在第 6014 行:

t 为滑块滑动的距离,i[$_CAHJd(182)] 为 challenge 的值:

跟进到 H 中,在第 704 行,将其作为全局变量导出:

window.userResponse = H;

t 值,challenge 值写入,控制台打印测试:

同样方法也可以将 ep 等导出:

window.getPasstime = ne[$_CJEZ(251)]["\u0024\u005f\u0043\u0043\u0043\u0071"];

passtime 值为 n,这里值为 374:

断住后向上跟栈到 $_CGlj 中,在第 8164 行生成:

n[$_DAAAV(871)] = $_Ii() - n[$_DAAAV(961)]

为滑块滑动开始到结束的时间:

rp 定义在第 6076 行,把 gt、32 位 challenge、passtime 通过 X 方法进行了加密:

// 混淆
o[$_CAIAt(791)] = X(i[$_CAIAt(104)] + i[$_CAIAt(182)][$_CAHJd(139)](0, 32) + o[$_CAHJd(704)]);

// 解混淆
o['rp'] = X(i['gt'] + i['challenge']['slice'](0, 32) + o['passtime']);

X 方法定义在第 1876 行:

同样在函数末尾,将其导出为全局变量: 

window.xFunc = X;
window.xFunc(i['gt'] + i['challenge']['slice'](0, 32) + o['passtime']);

对比测试,结果一致:

后面的直接先写成固定值,一开始的 l 参数复现大半:

现在把 V["encrypt"] 导出即可,局部搜索 var V =,只有一个结果,定义在第 2974 行,在函数结尾导出为全局变量:

window.getV_encrypt = V["encrypt"];

gt[$_CAIAt(218)](o) 先写为固定值,打印测试一下,成功得到结果:

h 参数逆向分析

拿到 l 值,再将 m[$_CAIAt(782)] 方法导出即可进一步拿到 h 值,在第 1568 行,同样导出为全局变量,控制台打印输出结果,成功得到 h 值: 

function getH(){
    return window.getM["\u0024\u005f\u0047\u0047\u0063"](window.getV_encrypt('{"lang":"zh-cn","userresponse":"6d06000dd600","passtime":269,"imgload":1193,"aa":"P,-,,,(!!@ypy!)Zy!)t!)!)!)yyXstsssxsussss(!!(k0020028/112.19/11CC:0)","ep":{"v":"7.8.9","$_BIB":false,"me":true,"tm":{"a":1668740199343,"b":0,"c":0,"d":0,"e":0,"f":1668740199352,"g":1668740199352,"h":1668740199352,"i":1668740199352,"j":1668740199352,"k":0,"l":1668740199357,"m":1668740199759,"n":1668740199760,"o":1668740199770,"p":1668740200226,"q":1668740200226,"r":1668740200229,"s":1668740200229,"t":1668740200229,"u":1668740200230},"td":-1},"vsof":"2515396075","rp":"3ced63451bb55c70951d6bbb5b851096"}', "c18a0f7fbb499af0"));
}
​
function getW(){
    return getH() + getU();
}

将值固定后对比,结果一致:

控制台打印,成功得到 w 值:

结果校验

猜你喜欢

转载自blog.csdn.net/Yy_Rose/article/details/127933247
今日推荐