声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
粉丝答疑又双叒来了!最近仍有不少粉丝咨询逆向相关问题,K哥会尽力回复,不过毕竟精力有限,不可能说是面面俱到(忙忘了@.@)。当然,还是会尽可能多筛选些典型的案例,产出相关文章,以供大伙学习交流。本期分析的验证码,如果没有相关业务,可能很多人不会接触到,也算是较为新颖,话不多说,开始正文:
逆向目标
目标:xx 邮政滑块逆向分析
网站:aHR0cHM6Ly9wYXNzcG9ydC4xMTE4NS5jbi9jYXMvbG9naW4=
<img src="https://v1.ax1x.com/2024/08/12/7G45fw.png" alt="3.png" style="zoom:50%;" />
抓包分析
抓包分析,先看图片接口 gen:
返回的内容比较多,后面估计会用到,我们留意一下就行,clientUid 后文分析:
再来看验证接口,即发验证码的接口:
- id : 图片接口返回;
- bgImageHeight: 写死即可;
- bgImageWidth:写死即可;
- clientUid:同图片接口参数 clientUid;
- deviceId:图片接口返回 deviceId;
- enmiid:需要分析;
- entSlidingTime:结束验证时间,模拟一下,校验不严;
- print:需要分析;
- sliderImageHeight:写死即可;
- sliderImageWidth:写死即可;
- startSlidingTime:开始验证时间,模拟一下,校验不严;
- trackList:轨迹列表。
验证结果:
- 失败
- 成功
逆向分析
gen 接口
该接口有个 clientUid 参数,目测是 uuid 类型, 我们全局搜索一下,发现只在 main.js 文件中存在,进去后发现被 ob 混淆:
又是经典的 ob 混淆, 这里直接借用 v佬 的插件快速还原,工具地址如下:
Github 仓库:https://github.com/cilame/v_jstools
在线分析工具:https://astexplorer.net
还原之前还需要稍微的预处理一下,比如下面这样一堆常量的花指令,以及十六进制形式的数字:
比如下面这样一堆常量的花指令,以及十六进制形式的数字
附上简单的预处理 AST 代码:
// 字符串美化
const simplifyLiteral = {
NumericLiteral({ node }) {
if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
node.extra = undefined;
}
},
StringLiteral({ node }) {
if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
node.extra = undefined;
}
},
}
traverse(ast, simplifyLiteral);
let my_obj = {};
const merge_odj = {
VariableDeclarator(path){
let {init} = path.node;
if (!types.isObjectExpression(init)) return;
let {properties} = init;
properties.forEach(property => {
// 确保属性值是字面量
if (types.isLiteral(property.value)) {
my_obj[property.key.name] = property.value;
}
});
}
};
traverse(ast, merge_odj);
const clearObjectofValue = {
MemberExpression(path){
let {object, property} = path.node
if (!property || !types.isIdentifier(property) || !types.isIdentifier(object)) return;
let key = property.name
let value = my_obj[key]
if (!value) return;
console.log(`${path.toString()} --> ${value.value}`)
path.replaceWithSourceString(value.value)
}
}
traverse(ast, clearObjectofValue);
ast = parser.parse(generator(ast,{compact:true}).code)
remove_no_citation_obj = {
VariableDeclarator(path){
let {init,id} = path.node
if (!types.isObjectExpression(init) && !types.isIdentifier(id)) return;
let {scope} = path
let binding = scope.getBinding(id.name)
if (binding.referencePaths.length !== 0) return;
path.remove()
}
}
// 删除未被引用的对象
traverse(ast,remove_no_citation_obj)
然后再利用 v_jstools 插件中的解密高级 ob 混淆即可还原了,good:
利用浏览器的替换功能,替换 main.js 文件,注意将代码压缩替换
,我们再来搜索 clientUid 看看:
返回 _0x349b8e
,我们再进去到 _0x236e93
函数中看看:
发现确实是 uuid 的构造,这边引库或者扣代码都行:
- clientUid
var _0x5e9294;
var _0x31a6e3 = new Uint8Array(16);
function _0x2f762f() {
if (!_0x5e9294 && !(_0x5e9294 = typeof crypto != "undefined" && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto != "undefined" && typeof msCrypto.getRandomValues == "function" && msCrypto.getRandomValues.bind(msCrypto))) {
throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
}
return _0x5e9294(_0x31a6e3);
}
for (var _0x22667e = function (_0x37736f) {
return typeof _0x37736f == "string" && /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i.test(_0x37736f);
}, _0x49863c = [], _0x18d3c6 = 0; _0x18d3c6 < 256; ++_0x18d3c6) {
_0x49863c.push((_0x18d3c6 + 256).toString(16).substr(1));
}
function _0x19f649(_0x3d7e8f) {
var _0x238244 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var _0x313ed4 = (_0x49863c[_0x3d7e8f[_0x238244 + 0]] + _0x49863c[_0x3d7e8f[_0x238244 + 1]] + _0x49863c[_0x3d7e8f[_0x238244 + 2]] + _0x49863c[_0x3d7e8f[_0x238244 + 3]] + "-" + _0x49863c[_0x3d7e8f[_0x238244 + 4]] + _0x49863c[_0x3d7e8f[_0x238244 + 5]] + "-" + _0x49863c[_0x3d7e8f[_0x238244 + 6]] + _0x49863c[_0x3d7e8f[_0x238244 + 7]] + "-" + _0x49863c[_0x3d7e8f[_0x238244 + 8]] + _0x49863c[_0x3d7e8f[_0x238244 + 9]] + "-" + _0x49863c[_0x3d7e8f[_0x238244 + 10]] + _0x49863c[_0x3d7e8f[_0x238244 + 11]] + _0x49863c[_0x3d7e8f[_0x238244 + 12]] + _0x49863c[_0x3d7e8f[_0x238244 + 13]] + _0x49863c[_0x3d7e8f[_0x238244 + 14]] + _0x49863c[_0x3d7e8f[_0x238244 + 15]]).toLowerCase();
if (!_0x22667e(_0x313ed4)) {
throw TypeError("Stringified UUID is invalid");
}
return _0x313ed4;
}
function _0x236e93(_0x5bbf46, _0x3b7cda, _0x21cf16) {
var _0x574141 = (_0x5bbf46 = _0x5bbf46 || {}).random || (_0x5bbf46.rng || _0x2f762f)();
_0x574141[6] = _0x574141[6] & 15 | 64;
_0x574141[8] = _0x574141[8] & 63 | 128;
if (_0x3b7cda) {
_0x21cf16 = _0x21cf16 || 0;
for (var _0x3503fc = 0; _0x3503fc < 16; ++_0x3503fc) {
_0x3b7cda[_0x21cf16 + _0x3503fc] = _0x574141[_0x3503fc];
}
return _0x3b7cda;
}
return _0x19f649(_0x574141);
};
console.log(_0x236e93());
验证接口
重点就是 enmiid、print 参数的生成,我们通过搜索 enmiid、print 即可定位到:
从 _0x5e712c
对象中取的值,我们可以搜索 _0x5e712c
的赋值操作 "_0x5e712c ="
, 即可定位到目标位置:
可以看到目标的参数以及都生成好了,我们可以从前往后开始分析:
_0x5b4cc9
就是图片返回的内容,取了 webAName、miid、rc, 然后拼接了一个 weba.js 请求这个链接,通过三次 then 得到 _0x383eb4
这个对象,进去到 _0x383eb4
对象的方法或者去看 weba.js 这个文件就可以发现又是 WebAssembly 与 JavaScript 之间进行交互,跟上篇某东 m 端的 wasm 大差不差,敢兴趣的可以去看看,我们接着向下分析,通过 _0x383eb4
对象下的函数对 _0x2d1fb0
、_0x354e64
进行一系列的操作最终得到了目标参数。
_0x2d1fb0
就是取的 rc,_0x354e64
是一个对象,里面的 hitId 对应 miid、deviceId 对应图片接口返回的 deviceId、_print
通过 _0x468795(_0x34c930)
返回,_0x34c930
对应 miid,然后进入到 _0x468795
函数:
通过 WebGL 在画布上绘制图形,并最终将绘制的内容转换为图像数据URL, 然后 _0x492d6a()
函数生成密文
最后再将密文与 _0x5ad970
+ "|" + 密文得到我们最终返回的数据,我们断点进入到 _0x492d6a()
函数:
可能有大佬一眼就看出来是什么加密了,也可以根据长度判断,我们也可以找一下是谁调用的,在 249 中,
我们直接找 (249) ,就在下面,加密也写出来了,我们验证一下没有问题:
最后就是 _0x383eb4
对象了,我们可以仿照着写,先请求 weba.js 链接,注意 weba.js 链接是动态:
"https://kks.11185.cn/static/front/" + webAName + "/bin/weba.js"
保存发现:
已经帮我们把对象给导出了,如下操作即可:
weba = require('./Weba.js');
window = global;
function ZxptRestKks(rc, _0x354e64){
var result;
var flag = false;
weba().then(function(Module) {
if (Module) {
_0x2d1fb0 = rc; // rc
window[_0x354e64.deviceId] = function () {
return _0xa2b2bc = Module.allocateUTF8(_0x2d1fb0);
};
// 访问 PromiseResult 下的属性
_0x156e64 = Module.allocateUTF8(JSON.stringify(_0x354e64));
_0x3e92b3 = Module._rotate(_0x156e64);
_0x57276e = Module.UTF8ToString(_0x3e92b3);
result = JSON.parse(_0x57276e);
flag = true;
}
}).catch(function(error) {
// 处理 Promise 被拒绝的情况
console.error(error);
});
while (!flag) {
require('deasync').sleep(100);
}
;
return result;
};
最后就是一些细节风控的处理,该验证码对距离的识别要求较高,轨迹要求不高,会封 ip 和 ua,速度快了,也会影响成功率,在 cv2 识别后,加上延迟即可,成功率 99%。附上处理代码:
- cv2 识别
def identify_gap(bg, tp):
"""
bg: 背景图片
tp: 缺口图片
out: 输出图片
"""
# 读取背景图片和缺口图片
bg_img = cv2.imdecode(np.frombuffer(bg, np.uint8), cv2.IMREAD_GRAYSCALE)
tp_img = cv2.imdecode(np.frombuffer(tp, np.uint8), cv2.IMREAD_GRAYSCALE) # 缺口图片
yy = []
xx = []
for y in range(tp_img.shape[0]):
for x in range(tp_img.shape[1]):
r = tp_img[y, x]
if r < 200:
yy.append(y)
xx.append(x)
tp_img = tp_img[min(yy):max(yy), min(xx):max(xx)]
# 识别图片边缘
bg_edge = cv2.Canny(bg_img, 100, 200)
tp_edge = cv2.Canny(tp_img, 100, 200)
# 转换图片格式
bg_pic = cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB)
tp_pic = cv2.cvtColor(tp_edge, cv2.COLOR_GRAY2RGB)
# 缺口匹配
res = cv2.matchTemplate(bg_pic, tp_pic, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) # 寻找最优匹配
# # 绘制方框
th, tw = tp_pic.shape[:2]
tl = max_loc # 左上角点的坐标
br = (tl[0] + tw, tl[1] + th) # 右下角点的坐标
cv2.rectangle(bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形
cv2.imwrite('distinguish.jpg', bg_img) # 保存在本地
# 返回缺口的X坐标
print(f'value: {tl[0]}')
return max_loc[0]
value = identify_gap(requests.get(backgroundImage).content, requests.get(sliderImage).content)
value = int(value * 0.55)
print(value)
time.sleep(1)
- 轨迹构造
def get_sub_trajectory(trajectories, value):
# 检查value是否在轨迹的x值中
for trajectory in trajectories:
if trajectory['x'] == value:
# 如果找到,截取从轨迹开始到该点的子数组
return [t for t in trajectories if t['x'] <= value]
# 如果value不在x值中,找到最接近value的x值
closest_x = None
min_diff = float('inf')
for trajectory in trajectories:
if abs(trajectory['x'] - value) < min_diff:
min_diff = abs(trajectory['x'] - value)
closest_x = trajectory['x']
# 截取从轨迹开始到最接近的x值的子数组
return [t for t in trajectories if t['x'] <= closest_x]
# 调用函数
# trackList : 浏览器生成的最大距离轨迹
value = 128 # 假设我们要找的value是 128
sub_trajectory = get_sub_trajectory(trackList, value)
sub_trajectory[-1]['type'] = 'up'
print(sub_trajectory)