某御验证码逆向分析

admin 2026-01-07 02:37:55 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章详细复盘了某御验证码的逆向过程:先通过auth接口的sign参数分析发现其基于固定配置+时间戳+随机数做MD5签名,再对加密混淆的jgbCaptcha.js进行AST还原字符串与函数调用,成功提取图片切片顺序算法并还原底图;随后定位check接口的report参数实为RSA加密轨迹,给出滑块识别与轨迹伪造代码,最终形成完整绕过方案。 综合评分: 85 文章分类: 逆向分析,WEB安全,CTF,漏洞分析,安全开发


这个sign函数就是将固定值对象_0x392c78取值进行生成签名,然后加密明文有时间戳,所以就有了接口的有效性校验。生成签名之后最后赋值给对象返回

 复制代码 隐藏代码
functionsign(_0x392c78) {
const _0x2639ff = _0x5354ca; // 混淆代码残留
// 构造一个对象,包含多种参数,用于生成签名
const _0x18ee40 = {
    appId: _0x392c78["aid"],                        // 应用ID,从传入参数获取
    type: 1,                                        // 固定类型值,这里为1
    version: _0x392c78["version"],                  // 版本号,从传入参数获取
    pn: _0x392c78.pn,                               // 产品名或包名,从传入参数获取
    os: _0x392c78.os,                               // 操作系统标识,从传入参数获取
    sdkName: _0x392c78["sdkName"],                  // SDK名称,从传入参数获取
    timestamp: Math.round(newDate()["getTime"]()),// 当前时间戳(毫秒),取整
    // 生成一个随机数nonce,先取当前时间戳,后加一个0~1亿的随机整数
    nonce: Math["round"](newDate()["getTime"]()) + Math["floor"](100000000 * Math["random"]()),
    ui: null,                                       // 未知参数,赋值null
    rc: 0,                                          // 计数参数,初始为0
    pc: 0,                                          // 计数参数,初始为0
    ec: 0,                                          // 计数参数,初始为0
    hc: 0,                                          // 计数参数,初始为0
    xc: 0,                                          // 计数参数,初始为0
    dc: 0,                                          // 计数参数,初始为0
    phone: _0x392c78["phone"]                        // 手机号,从传入参数获取
  };
// 用于拼接字符串以计算签名
let _0x3ca1de = '';
// 获取上面对象的所有键名并排序
const _0x2a4d77 = Object["keys"](_0x18ee40).sort();
// 遍历排序后的键,把“键名+键值”拼接成字符串
for&nbsp;(let&nbsp;_0x1ee694 =&nbsp;0; _0x1ee694 < _0x2a4d77["length"]; _0x1ee694++) {
&nbsp; &nbsp; _0x3ca1de +=&nbsp;String(_0x2a4d77[_0x1ee694]) +&nbsp;String(_0x18ee40[_0x2a4d77[_0x1ee694]]);
&nbsp; }
// 对拼接好的字符串进行MD5加密,得到签名,赋值给对象的 sign 属性
&nbsp; _0x18ee40["sign"] =&nbsp;md5(_0x3ca1de);
// 返回这个完整的参数对象(包含签名)
return&nbsp;_0x18ee40;
};

图片下载下来之后发现是乱序图片

convertArray函数:

&nbsp;复制代码&nbsp;隐藏代码
functionconvertArray(_0x8e4a4c, _0x1a03c8) {
// _0x8e4a4c: 输入的数组
// _0x1a03c8: 回调函数,参数通常是 (元素值, 索引)
// 初始化循环变量 _0x36c299 = 0,_0x1a01d8 是数组长度
// _0x1cd0cb 是新数组,用于存储转换后的结果
for&nbsp;(var&nbsp;_0x36c299 =&nbsp;0, _0x1a01d8 = _0x8e4a4c["length"], _0x1cd0cb = []; _0x2dac90.GpUbe(_0x36c299, _0x1a01d8); _0x36c299++) {
&nbsp; &nbsp;&nbsp;// _0x2dac90.GpUbe 用于判断循环条件,类似于 (_0x36c299 < _0x1a01d8)
&nbsp; &nbsp;&nbsp;// 对数组的每个元素调用回调函数 _0x1a03c8,传入当前元素和索引
&nbsp; &nbsp;&nbsp;// 将回调函数返回值赋值给新数组对应位置
&nbsp; &nbsp; _0x1cd0cb[_0x36c299] = _0x2dac90["WyKvQ"](_0x1a03c8, _0x8e4a4c[_0x36c299], _0x36c299);
&nbsp; &nbsp;&nbsp;// _0x2dac90.WyKvQ 表示调用函数 _0x1a03c8,传入当前元素和索引
&nbsp; }
// 返回转换后的新数组
return&nbsp;_0x1cd0cb;
};

还原代码分析: 底图是被切成32个垂直切片后打乱顺序的,还原的关键在于:

  1. 从图片URL中提取文件名
  2. 根据文件名计算出切片的正确顺序
  3. 按正确顺序重新拼接图片

算法流程图

node底图还原遇到了个问题就是,在浏览器环境中,可以通过 img.src = url 来设置图片源,但在 node-canvas 中,loadImage 返回的 Image 对象不支持直接设置 src 属性。用py复现还原底图其实更为方便,后续也是可以转成py的,笔者太懒,不想转换了。

check接口

搜索report参数,断点直接断住了

length就是滑块距离,重点分析report,report就是rarr传进enc函数返回的值

全局搜索rarr,分析代码。

代码就是监听鼠标事件获取轨迹

&nbsp;复制代码&nbsp;隐藏代码
try&nbsp;{
// 获取鼠标的横坐标 pageX
let&nbsp;_0x3f5faf = _0x3ff692.pageX;
// 获取鼠标的纵坐标 pageY
let&nbsp;_0x4420f9 = _0x3ff692["pageY"];
// 如果在移动端环境,则从 touches[0] 中读取 pageX 和 pageY,并向下取整
_0x4a2e4d("checkMobileEnv")() && (
&nbsp; &nbsp; _0x3f5faf =&nbsp;Math["floor"](_0x3ff692["touches"][0]["pageX"]),
&nbsp; &nbsp; _0x4420f9 =&nbsp;Math["floor"](_0x3ff692.touches[0]["pageY"])
&nbsp; );
// 如果 pos 属性还没有赋值,则赋值为本次事件的起点坐标(减去滑块的左偏移获得鼠标相对于滑块的 X 坐标,Y 坐标用 getBoundingClientRect().y)
&nbsp; !_0xe91a68["pos"] && (_0xe91a68["pos"] = {
&nbsp; &nbsp;&nbsp;x: _0x3f5faf -&nbsp;this["offsetLeft"],
&nbsp; &nbsp;&nbsp;y:&nbsp;this.getBoundingClientRect().y
&nbsp; });
// 如果之前 marginLeft 不存在,则给 rarr[0][0] 对象加上 y 坐标,记录下拖动开始的位置
&nbsp; !_0x511e91 && (_0xe91a68["rarr"][0][0].y&nbsp;= _0x4420f9);
// 给当前拖动对象设置样式:阴影、边框和 loading 背景图
this["style"]["boxShadow"] =&nbsp;"-1px 0px 2px rgba(26,150,82,0.3)";
this["style"]["border"] =&nbsp;"1px&nbsp;#00C95A";
this["style"]["backgroundImage"] =&nbsp;"url("&nbsp;+&nbsp;_0x4a2e4d("imgSliderLoading") +&nbsp;")";
// 获取 id 为 "slider-cover" 的元素,并设置其边框颜色
const&nbsp;_0x11fed4 =&nbsp;_0x4a2e4d("getElements")("id",&nbsp;"slider-cover");
&nbsp; _0x11fed4["style"].border&nbsp;=&nbsp;"1px solid&nbsp;#45D887";
// 如果 pos 还没有值,则将 slider-box 的 marginLeft 归零
if&nbsp;(!_0xe91a68["pos"]) {
&nbsp; &nbsp;&nbsp;_0x4a2e4d("getElements")("class",&nbsp;"slider-box")["style"]["marginLeft"] =&nbsp;"0";
&nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp;&nbsp;// 计算当前鼠标/手指与初始点击点的横向距离
&nbsp; &nbsp;&nbsp;const&nbsp;_0x3420d0 = _0x3f5faf - _0xe91a68["pos"].x;
&nbsp; &nbsp;&nbsp;// 如果小于 0,则 marginLeft 赋值为 0px(不允许向左拖动)
&nbsp; &nbsp;&nbsp;if&nbsp;(_0x3420d0 <&nbsp;0) {
&nbsp; &nbsp; &nbsp;&nbsp;_0x4a2e4d("getElements")("class",&nbsp;"slider-box")["style"]["marginLeft"] =&nbsp;`${0}px`;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// 如果大于 250,则 marginLeft 赋值为 250px(最大滑动距离)
&nbsp; &nbsp;&nbsp;elseif&nbsp;(_0x3420d0 >&nbsp;250) {
&nbsp; &nbsp; &nbsp;&nbsp;_0x4a2e4d("getElements")("class",&nbsp;"slider-box")["style"].marginLeft&nbsp;=&nbsp;250&nbsp;+&nbsp;"px";
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// 如果是刚刚开始拖动,则 marginLeft 赋值为 0px
&nbsp; &nbsp;&nbsp;elseif&nbsp;(_0xe91a68.events[0] ===&nbsp;"dragStart") {
&nbsp; &nbsp; &nbsp;&nbsp;_0x4a2e4d("getElements")("class",&nbsp;"slider-box").style["marginLeft"] =&nbsp;0&nbsp;+&nbsp;"px";
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// 否则,将 marginLeft 设置为当前拖动距离
&nbsp; &nbsp;&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp;&nbsp;_0x4a2e4d("getElements")("class",&nbsp;"slider-box")["style"].marginLeft&nbsp;=&nbsp;`${_0x3420d0}px`;
&nbsp; &nbsp; }
&nbsp; }
}&nbsp;catch&nbsp;(_0x5d0b9d) {
// 如果 try 里面出错,则打印错误并调用 universalLogCallback 日志上报
console["error"](_0x5d0b9d);
_0x4a2e4d("universalLogCallback")(2015);
}

rarr解决之后直接进入enc函数分析,就是传入轨迹然后跟之前auth接口的返回值进行了各种加密拼接

对轨迹进行的encryptLong加密经分析就是RSA加密,没有魔改,直接套库就行。

滑块识别代码

&nbsp;复制代码&nbsp;隐藏代码
defdetect_slide_distance(bg_url:&nbsp;str, front_url:&nbsp;str) ->&nbsp;int:
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; 使用ddddocr识别滑块缺口位置
&nbsp; &nbsp; Args:
&nbsp; &nbsp; &nbsp; &nbsp; bg_url: 背景图URL
&nbsp; &nbsp; &nbsp; &nbsp; front_url: 滑块图URL
&nbsp; &nbsp; Returns:
&nbsp; &nbsp; &nbsp; &nbsp; 滑动距离(px)
&nbsp; &nbsp; """
&nbsp; &nbsp;&nbsp;# 下载图片
&nbsp; &nbsp; bg_image = requests.get(bg_url).content
&nbsp; &nbsp; front_image = requests.get(front_url).content
&nbsp; &nbsp; det = ddddocr.DdddOcr(det=False, ocr=False,show_ad=False)
&nbsp; &nbsp; result = det.slide_match(front_image, bg_image, simple_target=True)
&nbsp; &nbsp;&nbsp;return&nbsp;result['target'][0]

轨迹模拟代码

轨迹代码只做参考,兄弟们可以去研究一下其他的轨迹,他这个就一个轨迹进行了加密,有可能对轨迹校验比较严格。

&nbsp;复制代码&nbsp;隐藏代码
defgenerate_human_track(distance:&nbsp;int, start_y:&nbsp;int&nbsp;=&nbsp;200, overshoot:&nbsp;bool&nbsp;=&nbsp;True) ->&nbsp;list:
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; 生成拟人轨迹
&nbsp; &nbsp; Args:
&nbsp; &nbsp; &nbsp; &nbsp; distance: 滑动距离(px)
&nbsp; &nbsp; &nbsp; &nbsp; start_y: 起始Y坐标
&nbsp; &nbsp; &nbsp; &nbsp; overshoot: 是否过冲

&nbsp; &nbsp; Returns:
&nbsp; &nbsp; &nbsp; &nbsp; 轨迹列表 [{"0": {"t": timestamp, "y": y}}, ...]
&nbsp; &nbsp; """
&nbsp; &nbsp; track = []
&nbsp; &nbsp; current_x =&nbsp;0
&nbsp; &nbsp; current_y = start_y
&nbsp; &nbsp; start_time =&nbsp;int(time.time() *&nbsp;1000)
&nbsp; &nbsp; current_time = start_time
&nbsp; &nbsp; overshoot_distance = random.uniform(3,&nbsp;8)&nbsp;if&nbsp;overshoot&nbsp;else0
&nbsp; &nbsp; target = distance + overshoot_distance
&nbsp; &nbsp; velocity =&nbsp;0
&nbsp; &nbsp;&nbsp;# 起始点
&nbsp; &nbsp; track.append({"0": {"t": current_time,&nbsp;"y": current_y}})
&nbsp; &nbsp;&nbsp;# 前进阶段
&nbsp; &nbsp;&nbsp;while&nbsp;current_x < target:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;current_x < distance *&nbsp;0.7:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; acceleration = random.uniform(1.5,&nbsp;3)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; acceleration = random.uniform(-1,&nbsp;0.5)
&nbsp; &nbsp; &nbsp; &nbsp; velocity =&nbsp;max(0.5, velocity + acceleration *&nbsp;0.1)
&nbsp; &nbsp; &nbsp; &nbsp; velocity =&nbsp;min(velocity,&nbsp;15)
&nbsp; &nbsp; &nbsp; &nbsp; step = velocity * random.uniform(0.8,&nbsp;1.2)
&nbsp; &nbsp; &nbsp; &nbsp; step =&nbsp;min(step, target - current_x)
&nbsp; &nbsp; &nbsp; &nbsp; current_x += step
&nbsp; &nbsp; &nbsp; &nbsp; current_y += random.uniform(-1.5,&nbsp;1.5)
&nbsp; &nbsp; &nbsp; &nbsp; current_time += random.randint(10,&nbsp;20)
&nbsp; &nbsp; &nbsp; &nbsp; track.append({str(len(track)): {"t": current_time,&nbsp;"y":&nbsp;round(current_y,&nbsp;2)}})
&nbsp; &nbsp;&nbsp;# 回调阶段
&nbsp; &nbsp;&nbsp;if&nbsp;overshoot&nbsp;and&nbsp;current_x > distance:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;current_x > distance:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; step = random.uniform(0.5,&nbsp;2)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; step =&nbsp;min(step, current_x - distance)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; current_x -= step
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; current_y += random.uniform(-0.3,&nbsp;0.3)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; current_time += random.randint(20,&nbsp;40)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; track.append({str(len(track)): {"t": current_time,&nbsp;"y":&nbsp;round(current_y,&nbsp;2)}})
&nbsp; &nbsp;&nbsp;return&nbsp;track

识别结果:

emmm,感觉成功率有点低哈,目前还不晓得问题出在哪里,可能是轨迹问题,也有可能还有风控吧(俺没找到),也没有说一定要上并发,毕竟只是练习题,练习练习能出值就行了。

结尾

emmm,思考ing…

-官方论坛

www.52pojie.cn

👆👆👆

公众号设置“星标”,不会错过新的消息通知

开放注册、精华文章和周边活动等公告


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:吾爱破解论坛 吾爱pojie《某御验证码逆向分析》

某御验证码逆向分析 网络安全文章

某御验证码逆向分析

文章总结: 文章详细复盘了某御验证码的逆向过程:先通过auth接口的sign参数分析发现其基于固定配置+时间戳+随机数做MD5签名,再对加密混淆的jgbCapt
评论:0   参与:  0