声明
本文章中所有内容仅供学习交流,相关链接做了脱敏处理,若有侵权,请联系我立即删除!
案例目标
极验验证码 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 中的抓包情况:
现对关键部分进行分析:
-
register-slide 注册滑动条请求,响应返回:
-
challenge: "d4e44298ed09f654b3c284a5fb6d72ad",动态变化,关键参数
-
gt: "019924a82c70bb123aae90d483087f94",固定值
-
-
gettype.php 获取验证码,Query String Parameters:GET 请求时,参数以 url string 的形式进行传递提交了 gt、callback:
-
gt:register-slide 时响应返回的定值
-
-
get.php 以 url string 的形式传递了一些参数:
-
gt: register-slide 时响应返回的定值
-
challenge: register-slide 时响应返回的定值
-
w:加密了,环境校验,轨迹
-
点击滑块后,响应返回的新数据接口:
-
ajax.php 以 url string 的形式传递了一些参数:
-
gt: register-slide 时响应返回的定值
-
challenge: register-slide 时响应返回的定值
-
w:加密了,环境校验,轨迹,跟前文 get.php 中的 w 值不一样
-
-
get.php 以 url string 的形式传递了一些参数:
-
gt: register-slide 时响应返回的定值
-
challenge: register-slide 时响应返回的定值
-
Preview 响应预览中 bg: 带缺口的背景图(乱码),3e72d088a.jpg:
-
fullbg:完整的背景原图(乱码),cd0bbb6fe.jpg:
-
slice:滑块图片,3e72d088a.png
-
-
ajax.php 以 url string 的形式传递了一些参数为:
-
gt: register-slide 时响应返回的定值
-
challenge: get.php 时响应返回
-
w:加密了,环境校验,轨迹,跟前文 ajax.php 中的 w 值不一样,至此三个 w 值均不一样
-
响应预览中,成功返回,message: "success",success: 1,validate 值:
-
响应预览中,失败返回 message: "fail",success: 0:
-
抓包步骤梳理分析:
-
进入页面,服务器响应返回一些参数(challenge、gt 等)和一些关键的 js 文件,用于生成图片及校验;
-
点击按钮进行验证后,会调用到第一步中的一些 js 文件,生成缺口图、完整背景图、滑块图、w 值等;
-
校验滑块是否对其缺口、轨迹是否异常、相关参数是否正确等,判断验证是否成功。
逆向调试
底图还原
点击按钮进行验证,会弹出滑动验证码,审查元素可以发现,底图是通过 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 值: