声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
粉丝答疑又双叒来了!最近仍有不少粉丝咨询逆向相关问题,K哥会尽力回复,不过毕竟精力有限,不可能说是面面俱到(忙忘了@.@)。当然,还是会尽可能多筛选些典型的案例,产出相关文章,以供大伙学习交流。本期分析的验证码,如果没有相关业务,可能很多人不会接触到,也算是较为新颖,话不多说,开始正文:
逆向目标
目标:xx 邮政滑块逆向分析
网站:aHR0cHM6Ly9wYXNzcG9ydC4xMTE4NS5jbi9jYXMvbG9naW4=
抓包分析
抓包分析,先看图片接口 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.nodeif (!property || !types.isIdentifier(property) || !types.isIdentifier(object)) return;let key = property.namelet 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.nodeif (!types.isObjectExpression(init) && !types.isIdentifier(id)) return;let {scope} = pathlet 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; // rcwindow[_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 = Nonemin_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)