滑块逆向-Tencent滑块逆向

网址https://cloud.tencent.com/product/captcha

滑完之后先看数据包

滑块验证流程分析

首先看加载滑块图片的请求,响应中有滑块的图片,图片是带缺口的,这个不重要,到时候放到打码平台上可以识别

参数看下

这里aid的参数是固定的,主要是ua这个参数,而且这个响应的sess字段会作为后面请求验证滑块的数据包的参数

并且验证滑块的请求参数中的[{"elem_id":1,"type":"DynAnswerType_POS","data":"374,54"}]里的data是缺口的坐标,这里的y左边54也是在请求滑块图片的响应包中

获取滑块图片参数逆向

ua其实就是user-agent做了个base64,很容易猜出来

滑块验证参数逆向

通过initiator找堆栈,找参数的过程很容易,最终定位到如下函数

关键参数在这里

1
2
3
4
5
6
7
c = {
collect: a,
tlg: a.length,
eks: s,
sess: this.sess,
ans: JSON.stringify(t)
}

pow_answer参数

该参数是在这一堆代码里生成的

1
2
3
u = this.workLoadData, d = u.workloadAns, l = u.workloadDuration, p = u.workloadNonce;
u.runWorkload && (c.pow_answer = null !== d && d.length > 0 ? "".concat(p).concat(d) : d,
c.pow_calc_time = l);

对c的赋值操作在

1
2
(c.pow_answer = null !== d && d.length > 0 ? "".concat(p).concat(d) : d,
c.pow_calc_time = l)

注意下这里的逻辑,针对 c.pow_answer = null !== d && d.length > 0 来说,是先执行 null !== d 得到一个布尔值,然后判断&& d.length > 0再得到一个布尔值,然后判断?里面的条件赋值,最后把赋值结果给c.pow_answer,这个解析的顺序涉及到js里面的运算符优先级

现在所需要的就是上面的pd是那里来的了,这两个变量最终都指向了this.workLoadData

此时p的值和d的值分别如下

这里首先考虑去搜索p和d的值,看是不是前面数据包返回的,如果是,可以节省很多工作量

然后去全局搜workLoadData,找到的地方不多,在能找到的地方打下断点,着重看下workloadAns属性的赋值

往下执行,可以发现workloadAns的赋值在r.workLoadData.workloadAns = "".concat(t.ans)这里

现在就是要看t是哪里来的,往上跟堆栈,t是这里的d传进来的,d最终是该函数的e参数经过操作得来的c = e.data, u = c.type, d = c.data

再往上,跟到一个PostMessage里面,在这里断下来,这个PostMessage是js的Web Worker机制,这里简单说一下什么是Web Worker,下面是一个demo

1
2
3
4
5
6
const worker = new Worker("encrypt-worker.js");

worker.postMessage({
type: "START",
data: "hello"
});

主线程使用 postMessage(...) 给 Worker 发送消息

Worker内部代码

1
2
3
4
onmessage = function(e) {
const message = e.data; // 就是 { type: "START", data: "hello" }
console.log(message.type); // "START"
}

Worker 监听 onmessage,收到的 e 就是一个 MessageEvent,里面的 .data 属性就是 postMessage(...) 发送的对象。

再回到目标代码,此时e.data的数据如下

1
2
3
4
5
6
7
8
{
"type": "TASK_RESULT",
"data": {
"ans": 8142,
"duration": 49,
"taskId": 2
}
}

要的是data字段,也就是PostMessage里面的{data:n}的n,n是这里PostMessage函数的参数,跟到PostMessage的上层

可以看到n就是这里面这一堆生成的

1
2
3
4
r(r({}, (0,
a.getWorkloadResult)(c)), {
taskId: u
})

再往上跟,可以看到这里面的data: e是需要的

e是.run方法的参数,可以在这里下断点

然后断到这里,查看上层函数,发现断在这里的时候的上层就是workerLoader.run方法

通过调试发现的现象就是,经过了这个.run方法之后,后面的ans就有值了,另外这里在临时打包的js的上层函数也打了断点,为啥要在这里打断点呢?我考虑的是既然临时这里断不下来,那就在上层函数那里断点看看

继续往上看临时打包的js的上层函数,也就是上图函数的上层函数

可以发现临时的临时的js就是这里的导出函数被执行之后才生成的,生成临时这里可以当成题外话

现在重点看run方法里面是怎么生成ans的值的,重新请求滑块,看一下断点的执行流程

首先走到了refreshWorkload方法里

然后是加载临时的那段js

然后调用run方法

最后发现生成值的地方在之前PostMessage的上层也就是这里,这里其实不好debugger下来,因为是临时生成的js代码,这里是通过先断在生成临时js代码的上层之后再搜索字符串,然后再debugger(或者通过.run方法里面也有生成参数的getWorkloadResult方法,这里不再赘述了,那个地方更不好debugger下来)

1
2
3
4
r(r({}, (0,
a.getWorkloadResult)(c)), {
taskId: u
})

此时t.data里面还没有ans,执行完上面那一串代码之后就生成了ans

所以关键代码就在a.getWorkloadResult函数和参数c这里,c的值如下,这俩值都是之前请求拿到的

1
2
3
4
{
"target": "1e27e7d184cad2022c8fea2a525bdceb",
"nonce": "7bdd8ef02e322a51#"
}

现在就是要扣这个函数的代码了,把目标函数单独扣下来,剩下的就是缺什么就从js里copy什么就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 上面还有一些代码,边运行边补就行了
function getWorkloadResult(t, n) {
for (var e = t.nonce, r = t.target, o = +new Date, u = 0, a = "number" == typeof n ? n : 3e4; v("".concat(e).concat(u)) !== r && (u += 1,
!(+new Date - o > a)); )
;
return {
ans: u,
duration: +new Date - o
}
}
let t = {
"target": "1e27e7d184cad2022c8fea2a525bdceb",
"nonce": "7bdd8ef02e322a51#"
}
let n = undefined
let ans_result = getWorkloadResult(t,n)
console.log(ans_result)

最终的pow_answer参数就是把prefix和ans拼接起来就行了

sess参数分析

前面响应里有

eks参数分析

和collect一样的,环境都一样

collect参数分析

collect参数是o.getTdcData生成的,跟到这个函数里面

开始的a({ft: (0,i["default"])()})执行结果是undefined,执行一下可以发现最终生成参数的值是通过window.TDC.getData(!0)生成的,找到这个函数所在的js

其实这个js的url也是前面的请求返回的,这个很重要,js代码要动态生成,不同的js加密的结果也会不同

这里需要注意的一点是需要先进a({ft: (0,i["default"])()})里面,因为虽然这个函数返回的是undefined,但是该函数内部却是设置了一个后面加密所需的变量

所以在本地扣代码的时候,也需要先调用window.TDC.setData(t)然后再调用加密函数,t的值是固定值

1
2
3
{
"ft": "qf_7Pf__H"
}

这里其实js代码是收集了鼠标轨迹的,但是没有校验轨迹

tlg参数

collect参数的长度

pow_calc_time参数

和pow_answer参数一起生成的