【验证码逆向专栏】某验二代滑块验证码逆向分析
![](https://s1.ax1x.com/2022/11/25/zY5cgs.png)## 声明
**本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!**
**本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!**
## 逆向目标
- 目标:某验二代滑块验证码逆向分析
- 主页:`aHR0cDovL3d3dy5qc2dzai5nb3YuY246NTg4ODgvbWluaS9uZXR3ZWIvU01MaWJyYXJ5LmpzcA==`
- 说明:大多数逻辑其实和三四代都一样,相同的就简写了,有疑惑的地方可以看以前的文章
- [【验证码逆向专栏】某验三代滑块验证码逆向分析](https://mp.weixin.qq.com/s/KmjGX_4LHRzceZjgsPPugw)
- [【验证码逆向专栏】某验四代滑块验证码逆向分析](https://mp.weixin.qq.com/s/zNb1uyzwU7a5n-zfRbN3bw)
## 抓包情况
主页点击搜索就会跳出二代的验证码,`netWebServlet.json` 的请求,会返回 `challenge` 和 `gt`。
!(https://s1.ax1x.com/2023/01/10/pSeLUW4.png)
有个 `get.php` 的请求,返回了一个新的 `challenge`,这个请求之后的操作,都要用这个新的 `challenge`,不然是验证不成功的,其他的还有验证码背景图片、乱序图片地址、`c`、`s` 等值,之前写过三代的文章,都是类似的,这里就不一一分析了。
!(https://s1.ax1x.com/2023/01/10/pSmC19x.png)
然后是 `ajax.php` 验证是否通过,通过之后返回一个 `validate`,请求里同样是需要我们逆向的 `w` 参数:
!(https://s1.ax1x.com/2023/01/10/pSmCQ41.png)
!(https://s1.ax1x.com/2023/01/10/pSmCKE9.png)
然后同样还是 `netWebServlet.json` 接口,带上 `get.php` 请求返回的 `challenge` 以及 `ajax.php` 返回的 `validate`,请求拿到一个 `name` 的字段。
!(https://s1.ax1x.com/2023/01/10/pSmCMNR.png)
!(https://s1.ax1x.com/2023/01/10/pSmCnHJ.png)
后续的搜索数据,带上这个 `name` 就行了:
!(https://s1.ax1x.com/2023/01/10/pSmkSx0.png)
## 逆向分析
搞过三、四代的都知道我们可以直接搜索 `w` 的 Unicode 值 `\u0077` 即可定位,但是二代则不是 Unicode,而是16进制的编码,搜索 `\x77` 即可定位,当然按照正常流程,跟栈也能很容易找到加密的位置。
!(https://s1.ax1x.com/2023/01/10/pSmVKs0.png)
### 获取 H7z 值
从上图中可以知道 `w` 的值为 `r7z + H7z`,先看 `H7z`。
!(https://s1.ax1x.com/2023/01/10/pSmZoB6.png)
跟进这个方法,来到一大串控制流,这里还是推荐用 AST 还原一下,后续可能有一些循环啥的,硬跟的话容易出错,当然直接全部扣一把梭也是可以的,`H7z` 的核心其实就是 RSA 加密随机字符串,三代四代都有,这里就不细讲了。
!(https://s1.ax1x.com/2023/01/10/pSmmyYF.png)
### 获取 r7z 值
然后就是 `r7z`,主要由以下两句代码生成:
!(https://s1.ax1x.com/2023/01/11/pSmqt6s.png)
```javascript
q7z = n0B(h7B(Y7z), V7z())
r7z = p7B(q7z)
```
可以看到其中有个变量 `Y7z` 参与了计算,先来看看他是怎么来的,直接搜索即可定位,可以发现同样是16进制的编码,由五个值组成:`userresponse`、`passtime`、`imgload`、`aa`、`ep`
!(https://s1.ax1x.com/2023/01/11/pSmLyUf.png)
#### 获取 userresponse 值
挨个分析,首先是 `userresponse`,将滑动距离和 `challenge` 的值传入一个方法,得到一个 9 位字符串:
!(https://s1.ax1x.com/2023/01/11/pSmxtcF.png)
上图中 `g7z` 就是滑动距离,搜索可以看到定义的地方,尺子量一下对比一下,和滑动的距离是一致的:
!(https://s1.ax1x.com/2023/01/11/pSmzFu4.png)
!(https://s1.ax1x.com/2023/01/11/pSmzDbj.png)
然后再来看看那个方法,跟进去之后也是一大串 `switch-case` 控制流:
!(https://s1.ax1x.com/2023/01/11/pSmzLRK.png)
还原一下代码如下:
```javascript
function getUserResponse(L0z, o0z) {
for (var j0z = o0z.slice(32), c0z = [], X0z = 0; X0z < j0z.length; X0z++){
var K0z = j0z.charCodeAt(X0z);
c0z = K0z > 57 ? K0z - 87 : K0z - 48;
}
j0z = 36 * c0z + c0z;
var k0z = Math.round(L0z) + j0z;
o0z = o0z.slice(0, 32);
var n0z, f0z = [[], [], [], [], []], Q0z = {}, N0z = 0;
X0z = 0;
for (var i0z = o0z.length; i0z > X0z; X0z++){
n0z = o0z.charAt(X0z), Q0z || (Q0z = 1, f0z.push(n0z), N0z++, N0z = 5 == N0z ? 0 : N0z);
}
var y0z, v0z = k0z, B0z = 4, x0z = "", I0z = ;
while ( v0z > 0) {
v0z - I0z >= 0 ? (y0z = parseInt(Math.random() * f0z.length, 10),
x0z += f0z, v0z -= I0z) : (f0z.splice(B0z, 1),
I0z.splice(B0z, 1), B0z -= 1);
}
return x0z;
}
```
#### 获取 passtime值
`passtime`不用考虑是怎么通过函数获取的,含义就是滑动完成所花费的时间,直接取轨迹的最后一个值即可,这个也和三四代是一样的,获取语句为:`var passtime = track`,如下图所示,轨迹的最后一个值时间为 871,`passtime` 的值同样也为 871。
!(https://s1.ax1x.com/2023/01/11/pSnpZp6.png)
!(https://s1.ax1x.com/2023/01/11/pSnpEfx.png)
#### 获取 imgload 值
`imgload` 也没啥特别的,从字面意思猜测应该是图片加载耗时,实测直接写死即可,或者整个随机值就行。
#### 获取 aa 值
`aa` 的值就是 `F7z`,如下图所示:
!(https://s1.ax1x.com/2023/01/11/pSneG6A.png)
搜索 `F7z`,定位到下图所示的地方,向一个方法中传入了一个时间戳:
!(https://s1.ax1x.com/2023/01/11/pSnnkrR.png)
跟进去同样是 `switch-case` 控制流,需要注意的是下图中 `c7B(M9r.R8z(764), K1z)` 的值其实就是轨迹。
!(https://s1.ax1x.com/2023/01/11/pSnnDLn.png)
这段控制流还原一下就变成这样了:
```javascript
function getF7z(track){
var o5r = 6;
for (var N1z, X1z = s6z(track), f1z = [], B1z = [], o1z = [], t1z = 0, j1z = X1z.length; t1z < j1z; t1z++){
if (o5r * (o5r + 1) % 2 + 8) {
N1z = u6z(X1z),
N1z ? B1z.push(N1z) : (f1z.push(O6z(X1z)),
B1z.push(O6z(X1z))),
o1z.push(O6z(X1z));
o5r = o5r >= 17705 ? o5r / 3 : o5r * 3;
}
}
return f1z.join("") + "!!" + B1z.join("") + "!!" + o1z.join("");
}
function s6z(F6z){
for (var Y6z, g6z, a6z, E6z = [], D6z = 0, P6z = [], J6z = 0, l6z = F6z.length - 1; J6z < l6z; J6z++) {
Y6z = Math.round(F6z - F6z),
g6z = Math.round(F6z - F6z),
a6z = Math.round(F6z - F6z),
P6z.push(),
0 == Y6z && 0 == g6z && 0 == a6z || (0 == Y6z && 0 == g6z ? D6z += a6z : (E6z.push(), D6z = 0));
}
return 0 !== D6z && E6z.push(), E6z;
}
function O6z(r6z){
var d6z = "()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqr"
, m6z = d6z.length
, Z6z = ""
, H6z = Math.abs(r6z)
, W6z = parseInt(H6z / m6z);
W6z >= m6z && (W6z = m6z - 1), W6z && (Z6z = d6z.charAt(W6z)), H6z %= m6z;
var q6z = "";
return r6z < 0 && (q6z += "!"), Z6z && (q6z += "$"), q6z + Z6z + d6z.charAt(H6z);
}
function u6z(R6z){
for (var z6z = [, , , , , , , , ], h6z = 0, C6z = z6z.length; h6z < C6z; h6z++){
if (R6z == z6z && R6z == z6z){
return "stuvwxyz~"
}
}
return 0;
}
```
以上只是 `F7z` 第一次生成的地方,后面还有二次处理,如下图所示:
!(https://s1.ax1x.com/2023/01/11/pSnMvy4.png)
同样跟进去,三个传入的参数分别是第一次生成的 `F7z`、`get.php` 请求返回的 `c` 和 `s` 参数。
!(https://s1.ax1x.com/2023/01/11/pSnMjlF.png)
同样是一段控制流,还原后如下:
```javascript
function getF7z2(Q1z, v1z, T1z){
var i1z, x1z = 0, c1z = Q1z, y1z = v1z, k1z = v1z, L1z = v1z;
while (1){
if (i1z = T1z.substr(x1z, 2)){
x1z += 2;
var n1z = parseInt(i1z, 16)
, M1z = String.fromCharCode(n1z)
, I1z = (y1z * n1z * n1z + k1z * n1z + L1z) % Q1z.length;
c1z = c1z.substr(0, I1z) + M1z + c1z.substr(I1z);
}else {
return c1z
}
}
return Q1z
}
```
至此 `aa` 参数分析完毕!
#### 获取 ep 值
`ep` 的值就是一个版本号,此处是 `{'v': '6.0.9'}`,写死即可。
!(https://s1.ax1x.com/2023/01/11/pSn3V6U.png)
#### 获取 rp 值
自此 `Y7z` 的第一步生成就分析完毕了,注意接下来还有一步,向 `Y7z` 里新增了一个 `rp` 参数:
!(https://s1.ax1x.com/2023/01/11/pSnUNx1.png)
这个值的组成看起来很长,实际上是将 gt、challenge 前 32 位以及 passtime 相加经过 MD5 加密后得到的。
```javascript
Y7z["rp"] = md5(gt + challenge.slice(0, 32) + passtime)
```
!(https://s1.ax1x.com/2023/01/11/pSnU5dS.png)
上图中 `I0B` 就是 MD5 方法,跟进去其实是可以看到很多 MD5 特征的,如下图所示:
!(https://s1.ax1x.com/2023/01/11/pSnaeoD.png)
自此 `Y7z` 的值就搞定了,然后接着前面的看,也就是 `q7z` 的值,同样和三四代一样的,`encrypt` 是 AES 加密,`Y7z` 经过 `JSON.stringify()` 处理为字符串作为待加密对象,后面是 16 为随机字符串作为 AES 的 Key,注意这里的随机字符串应该和获取 `H7z` 值时的随机字符串一致,不然是验证不成功的。
!(https://s1.ax1x.com/2023/01/12/pSnvR4x.png)
然后下一步就是获取 `r7z` 的值,将上一步得到的 `q7z` 经过一个方法进行处理,跟进方法,又是和三四代一样的,熟悉的 `res + end`,如下图所示:
!(https://s1.ax1x.com/2023/01/12/pSupiad.png)
直接扣代码,或者直接使用三代的代码即可:
```javascript
function $_GJF(e) {
var t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()";
return e < 0 || e >= t["length"] ? "." : t["charAt"](e);
}
function $_HBO(e, t) {
return e >> t & 1;
}
function $_HCX(e, o) {
var i = this;
o || (o = i);
for (var t = function(e, t) {
for (var n = 0, r = 24 - 1; 0 <= r; r -= 1)
1 === $_HBO(t, r) && (n = (n << 1) + $_HBO(e, r));
return n;
}, n = "", r = "", s = e.length, a = 0; a < s; a += 3) {
var c;
if (a + 2 < s)
c = (e << 16) + (e << 8) + e,
n += $_GJF(t(c, 7274496)) + $_GJF(t(c, 9483264)) + $_GJF(t(c, 19220)) + $_GJF(t(c, 235));
else {
var _ = s % 3;
2 == _ ? (c = (e << 16) + (e << 8),
n += $_GJF(t(c, 7274496)) + $_GJF(t(c, 9483264)) + $_GJF(t(c, 19220)),
r = ".") : 1 == _ && (c = e << 16,
n += $_GJF(t(c, 7274496)) + $_GJF(t(c, 9483264)),
r = "." + ".");
}
}
return {
"res": n,
"end": r
};
}
```
### 获取 w 值
自此 `w` 的就已经出来了,`r7z + H7z` 即为 `w` 的值。
!(https://s1.ax1x.com/2023/01/12/pSu91mD.png)
## 结果验证
测试过掉验证码抓取数据成功:
!(https://s1.ax1x.com/2023/01/12/pSu9ykj.png) 我们都看不懂,大神辛苦了,休息下吧{:1_918:} 收藏学习,感谢楼主分享系列教程 楼主说一下三代点选吧,太难了 maroo 发表于 2023-2-23 16:03
楼主说一下三代点选吧,太难了
三代已经发了,可以查看了:lol 阿里的吗? 感谢楼主分享系列教程 尺子是啥软件 赶紧好难啊! 奈何家穷,读书不多,只能:卧槽,大神,牛碧呀。。。。