文章总结: 文章详细复盘了某御验证码的逆向过程:先通过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 (let _0x1ee694 = 0; _0x1ee694 < _0x2a4d77["length"]; _0x1ee694++) {
_0x3ca1de += String(_0x2a4d77[_0x1ee694]) + String(_0x18ee40[_0x2a4d77[_0x1ee694]]);
}
// 对拼接好的字符串进行MD5加密,得到签名,赋值给对象的 sign 属性
_0x18ee40["sign"] = md5(_0x3ca1de);
// 返回这个完整的参数对象(包含签名)
return _0x18ee40;
};
图片下载下来之后发现是乱序图片
convertArray函数:
复制代码 隐藏代码
functionconvertArray(_0x8e4a4c, _0x1a03c8) {
// _0x8e4a4c: 输入的数组
// _0x1a03c8: 回调函数,参数通常是 (元素值, 索引)
// 初始化循环变量 _0x36c299 = 0,_0x1a01d8 是数组长度
// _0x1cd0cb 是新数组,用于存储转换后的结果
for (var _0x36c299 = 0, _0x1a01d8 = _0x8e4a4c["length"], _0x1cd0cb = []; _0x2dac90.GpUbe(_0x36c299, _0x1a01d8); _0x36c299++) {
// _0x2dac90.GpUbe 用于判断循环条件,类似于 (_0x36c299 < _0x1a01d8)
// 对数组的每个元素调用回调函数 _0x1a03c8,传入当前元素和索引
// 将回调函数返回值赋值给新数组对应位置
_0x1cd0cb[_0x36c299] = _0x2dac90["WyKvQ"](_0x1a03c8, _0x8e4a4c[_0x36c299], _0x36c299);
// _0x2dac90.WyKvQ 表示调用函数 _0x1a03c8,传入当前元素和索引
}
// 返回转换后的新数组
return _0x1cd0cb;
};
还原代码分析: 底图是被切成32个垂直切片后打乱顺序的,还原的关键在于:
- 从图片URL中提取文件名
- 根据文件名计算出切片的正确顺序
- 按正确顺序重新拼接图片
算法流程图
node底图还原遇到了个问题就是,在浏览器环境中,可以通过 img.src = url 来设置图片源,但在 node-canvas 中,loadImage 返回的 Image 对象不支持直接设置 src 属性。用py复现还原底图其实更为方便,后续也是可以转成py的,笔者太懒,不想转换了。
check接口
搜索report参数,断点直接断住了
length就是滑块距离,重点分析report,report就是rarr传进enc函数返回的值
全局搜索rarr,分析代码。
代码就是监听鼠标事件获取轨迹
复制代码 隐藏代码
try {
// 获取鼠标的横坐标 pageX
let _0x3f5faf = _0x3ff692.pageX;
// 获取鼠标的纵坐标 pageY
let _0x4420f9 = _0x3ff692["pageY"];
// 如果在移动端环境,则从 touches[0] 中读取 pageX 和 pageY,并向下取整
_0x4a2e4d("checkMobileEnv")() && (
_0x3f5faf = Math["floor"](_0x3ff692["touches"][0]["pageX"]),
_0x4420f9 = Math["floor"](_0x3ff692.touches[0]["pageY"])
);
// 如果 pos 属性还没有赋值,则赋值为本次事件的起点坐标(减去滑块的左偏移获得鼠标相对于滑块的 X 坐标,Y 坐标用 getBoundingClientRect().y)
!_0xe91a68["pos"] && (_0xe91a68["pos"] = {
x: _0x3f5faf - this["offsetLeft"],
y: this.getBoundingClientRect().y
});
// 如果之前 marginLeft 不存在,则给 rarr[0][0] 对象加上 y 坐标,记录下拖动开始的位置
!_0x511e91 && (_0xe91a68["rarr"][0][0].y = _0x4420f9);
// 给当前拖动对象设置样式:阴影、边框和 loading 背景图
this["style"]["boxShadow"] = "-1px 0px 2px rgba(26,150,82,0.3)";
this["style"]["border"] = "1px #00C95A";
this["style"]["backgroundImage"] = "url(" + _0x4a2e4d("imgSliderLoading") + ")";
// 获取 id 为 "slider-cover" 的元素,并设置其边框颜色
const _0x11fed4 = _0x4a2e4d("getElements")("id", "slider-cover");
_0x11fed4["style"].border = "1px solid #45D887";
// 如果 pos 还没有值,则将 slider-box 的 marginLeft 归零
if (!_0xe91a68["pos"]) {
_0x4a2e4d("getElements")("class", "slider-box")["style"]["marginLeft"] = "0";
} else {
// 计算当前鼠标/手指与初始点击点的横向距离
const _0x3420d0 = _0x3f5faf - _0xe91a68["pos"].x;
// 如果小于 0,则 marginLeft 赋值为 0px(不允许向左拖动)
if (_0x3420d0 < 0) {
_0x4a2e4d("getElements")("class", "slider-box")["style"]["marginLeft"] = `${0}px`;
}
// 如果大于 250,则 marginLeft 赋值为 250px(最大滑动距离)
elseif (_0x3420d0 > 250) {
_0x4a2e4d("getElements")("class", "slider-box")["style"].marginLeft = 250 + "px";
}
// 如果是刚刚开始拖动,则 marginLeft 赋值为 0px
elseif (_0xe91a68.events[0] === "dragStart") {
_0x4a2e4d("getElements")("class", "slider-box").style["marginLeft"] = 0 + "px";
}
// 否则,将 marginLeft 设置为当前拖动距离
else {
_0x4a2e4d("getElements")("class", "slider-box")["style"].marginLeft = `${_0x3420d0}px`;
}
}
} catch (_0x5d0b9d) {
// 如果 try 里面出错,则打印错误并调用 universalLogCallback 日志上报
console["error"](_0x5d0b9d);
_0x4a2e4d("universalLogCallback")(2015);
}
rarr解决之后直接进入enc函数分析,就是传入轨迹然后跟之前auth接口的返回值进行了各种加密拼接
对轨迹进行的encryptLong加密经分析就是RSA加密,没有魔改,直接套库就行。
滑块识别代码
复制代码 隐藏代码
defdetect_slide_distance(bg_url: str, front_url: str) -> int:
"""
使用ddddocr识别滑块缺口位置
Args:
bg_url: 背景图URL
front_url: 滑块图URL
Returns:
滑动距离(px)
"""
# 下载图片
bg_image = requests.get(bg_url).content
front_image = requests.get(front_url).content
det = ddddocr.DdddOcr(det=False, ocr=False,show_ad=False)
result = det.slide_match(front_image, bg_image, simple_target=True)
return result['target'][0]
轨迹模拟代码
轨迹代码只做参考,兄弟们可以去研究一下其他的轨迹,他这个就一个轨迹进行了加密,有可能对轨迹校验比较严格。
复制代码 隐藏代码
defgenerate_human_track(distance: int, start_y: int = 200, overshoot: bool = True) -> list:
"""
生成拟人轨迹
Args:
distance: 滑动距离(px)
start_y: 起始Y坐标
overshoot: 是否过冲
Returns:
轨迹列表 [{"0": {"t": timestamp, "y": y}}, ...]
"""
track = []
current_x = 0
current_y = start_y
start_time = int(time.time() * 1000)
current_time = start_time
overshoot_distance = random.uniform(3, 8) if overshoot else0
target = distance + overshoot_distance
velocity = 0
# 起始点
track.append({"0": {"t": current_time, "y": current_y}})
# 前进阶段
while current_x < target:
if current_x < distance * 0.7:
acceleration = random.uniform(1.5, 3)
else:
acceleration = random.uniform(-1, 0.5)
velocity = max(0.5, velocity + acceleration * 0.1)
velocity = min(velocity, 15)
step = velocity * random.uniform(0.8, 1.2)
step = min(step, target - current_x)
current_x += step
current_y += random.uniform(-1.5, 1.5)
current_time += random.randint(10, 20)
track.append({str(len(track)): {"t": current_time, "y": round(current_y, 2)}})
# 回调阶段
if overshoot and current_x > distance:
while current_x > distance:
step = random.uniform(0.5, 2)
step = min(step, current_x - distance)
current_x -= step
current_y += random.uniform(-0.3, 0.3)
current_time += random.randint(20, 40)
track.append({str(len(track)): {"t": current_time, "y": round(current_y, 2)}})
return track
识别结果:
emmm,感觉成功率有点低哈,目前还不晓得问题出在哪里,可能是轨迹问题,也有可能还有风控吧(俺没找到),也没有说一定要上并发,毕竟只是练习题,练习练习能出值就行了。
结尾
emmm,思考ing…
-官方论坛
www.52pojie.cn
👆👆👆
公众号设置“星标”,您不会错过新的消息通知
如开放注册、精华文章和周边活动等公告
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:吾爱破解论坛 吾爱pojie《某御验证码逆向分析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论