HU航司, JS逆向,TD风控,加密算法纯算
2026/7/24 4:41:53 网站建设 项目流程

技术文章大纲:HU航司App逆向与验签协议算法分析

背景与目标
  • 目标网址:aHR0cHM6Ly9uZXcuaG5haXIuY29tL2hhaW5hbmFpci9pYmUvY29tbW9uL2xvZ2luLmRv

JS逆向分析流程

流程分析
获取验证码

打开登录页面,输入手机号点击获取验证码触发验证码,里面需要还原的值有q1-q2,p3-p9

清除浏览器cookie刷新网页重新请求并打上断点

往下跟栈,此时数据已生成。

q1:在文件里面搜索QO00Qo,可以看到这个值是一个变量+fmopt.token+fmopt.partner+fopt.appname, fmopt的属性值都可以写死,

搜索这个变量oOO0OQ,是个定值。

回到最初,这几个值拼接后进行url编码就是q1

q2:全局搜索o00ooQ,这个可以写死,有两种状态一种|^^1^^1^^1 然后url编码
一种 blackbox替换|然后url编码

var o00ooQ = (O0oOoQ.blackBox || '|') + '^^' + O0oOoQ.requestType + '^^' + (QoQ0oO(OOOQoQ.lang) + 1) + '^^' + (Q0QQO0.support_css3_value(QOOO0o('usbotgpsn-tuzmf'), QOOO0o('qsftfswf-3e')) && !(!!window.ActiveXObject || QOOO0o('AdujwfXOckfdu')in window) ? 1 : 2);

p3:经过位置加密函数生成,

未加密数据:搜索这个值a=函数(p1+p2)+validateCodeObj/| + userAnsewr/| + 函数(变量b)

var o00OQ0 = oOQoOo(QO00Qo + '^^' + o00ooQ) + '^^' + (O0oOoQ.validateCodeObj || '|') + '^^' + (O0oOoQ.userAnswer || '|') + '^^' + (O0oOoQ.validateToken || '|') + oOQoOo(QoQQ0O);

搜索变量b b=变量c/| + true/| +true/|+时间戳

var QoQQ0O = (Q00oo0 || '|') + '^^' + (Oo0O0O || '|') + '^^' + (O0Oo0O || '|') + '^^' + new Date().getTime();

搜索变量c

还原一下代码 c=argumentss['callee']['caller']['toString']['length']

var Q00oo0 = function() { return arguments[callee]caller][toString]()[length]; }();

秘钥 d=e('stq67pv9')+p1.substring(10,18),这里后面的属性是p1进行截取充当

var o0o0oo = QOOO0o('stq67pv9') + Q0QOoQ.substring(10, 18);

e函数,直接扣下来转成python,ASCII编码(每个小写的ASCII编码-1)相当于小写字母循环向前移动一位

p4:sensorInfo/| +mouseInfo/| + usedTime/|

然后p3和p4进行同一个函数加密

var oQooQQ = (O0oOoQ.sensorInfo || '|') + '^^' + (O0oOoQ.mouseInfo || '|') + '^^' + (O0oOoQ.usedTime || '|');

跟栈进入方法,可以明显看到是AES加密

p5:固定为 web

p6: OOQ00O(8);+window.location.href+秘钥 OOQ00O(8);是一个八位的随机数

var OQoQOQ = OOQ00O(8); var oO0oQo = Qo0QO0(OQoQOQ + window.location.href, o0o0oo);

p7:另外一个是函数(p6)+函数(p2)

QooQQO.p7 = QO0QOQ + OOQ00O(32); var QO0QOQ = oOQoOo(oO0oQo) + oOQoOo(QoQQ0O);

OOQ00O(32);是一个32随机数

p8: 八位随机数

QooQQO.p8 = OQoQOQ; var OQoQOQ = OOQ00O(8);

p9 函数(p2,秘钥)

函数---》以上的统称,全局搜索这个变量,发现是md5

验证接口

触发接口流程和获取一样,注重关注以下几点

p2:初始化状态是1,验证的状态是3

p3,里面的validateCodeObj是初始化返回的数据,里面存放id,背景图的url等信息。userAnswer里面存放移动距离,时间戳

往上再跟一层,里面的userAnswer是图片的缺口距离+固定10+当前时间

mouserInfo:里面存放的是页面的x,y。所以这个鼠标轨迹可以固定


验证码图片还原

保存完图片是这种切片,所以需要还原一下

重新触发验证码获取,初始化返回了一个imageType,可以全局搜一下这个字段,这里就是把返回的两行八列的图片转成原图。丢给ai转一下就行

  • 签名参数构造流程(时间戳、设备信息、密钥派生)
  • 算法还原(常见加密模式:AES、RSA、HMAC-SHA256等)

OPENVC识别距离
def extract_slider_mask_and_bbox(slider_img): """ 从滑块图片提取精确掩码和边界框。 优先使用 alpha 通道,否则用阈值二值化。 Returns: mask: 二值掩码 (h, w), 255=滑块区域 bbox: (x, y, w, h) 滑块有效区域的边界框 piece_bgr: 裁剪后的滑块 BGR 图像 piece_mask: 裁剪后的掩码 """ h, w = slider_img.shape[:2] channels = slider_img.shape[2] if len(slider_img.shape) > 2 else 1 if channels == 4: # PNG 有 alpha 通道,直接使用 alpha = slider_img[:, :, 3] _, mask = cv2.threshold(alpha, 128, 255, cv2.THRESH_BINARY) bgr = slider_img[:, :, :3] else: # 没有 alpha 通道,用阈值提取 gray = cv2.cvtColor(slider_img, cv2.COLOR_BGR2GRAY) if channels >= 3 else slider_img _, mask = cv2.threshold(gray, 30, 255, cv2.THRESH_BINARY) # 获取最大连通域(去除噪声) num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8) if num_labels > 1: largest_label = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA]) mask = np.where(labels == largest_label, 255, 0).astype(np.uint8) bgr = slider_img[:, :, :3] if channels >= 3 else cv2.cvtColor(slider_img, cv2.COLOR_GRAY2BGR) # 获取边界框 coords = cv2.findNonZero(mask) if coords is None: return mask, None, None, None x, y, w_box, h_box = cv2.boundingRect(coords) # 裁剪 piece_bgr = bgr[y:y + h_box, x:x + w_box] piece_mask = mask[y:y + h_box, x:x + w_box] bbox = (x, y, w_box, h_box) return mask, bbox, piece_bgr, piece_mask

验证结果


法律与伦理边界
  • 逆向技术的合法使用范围
  • 企业防护与用户隐私的平衡
  • 个人观点,禁止商业行为,侵权请联系下架

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询