某美验证码及风控浅析二

查看 84|回复 9
作者:wangguang   
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。
前言
第一部分跳转链接->https://www.52pojie.cn/thread-2069214-1-1.html
设备指纹风控部分
网站同一个设备重复登录操作十几次之后设备就会被风控,一段时间内无法登录。下面是接口返回值。而跟风控部分有关的就是web接口跟fverify接口。接下来就对两个接口有关风控的参数进行分析
{"msg":"您的操作存在风险,已被限制登录","result":{"msg":"您的操作存在风险,已被限制登录","code":-1},"code":-1}
产品介绍
以下内容取自于官网
产品简介
设备指纹,基于先进的人工智能技术,通过高稳定高兼容设备指纹反欺诈SDK,全路径布控和全栈式实时防御营销活动作弊、撞库盗号、渠道推广作弊、支付交易风险、内容盗爬、刷榜刷单、用户裂变等欺诈行为,护航客户营销ROI增长。
产品功能
设备唯一标识、篡改设备识别、虚拟机识别、伪造设备标识、积分墙风险识别、风险环境识别、行为风险识别。


2025-11-01-11-52-05-image.png (91.11 KB, 下载次数: 0)
下载附件
2025-11-1 20:01 上传

因为笔者搞的是web,而且还不是最新版本的,笔者也搞不懂自己搞的是哪个版本,看官网也就是为了知己知彼,看看他究竟会取哪些东西。笔者搞了几天风控,觉得这玩意他得测啊,他不像某些加密,你模拟的一模一样他就给他返回值了,只能把所有返回后端的数据都模拟了,逐步测试才知道哪些东西校验的,我这边看了数某官网,于是对SDK 收集数据的类型:Canvas指纹、cookies信息、ua客户端浏览器信息这些参数着重模拟。
加密分析
web接口的传参总共就一个加密。


2025-11-01-11-36-29-image.png (82.9 KB, 下载次数: 0)
下载附件
2025-11-1 20:01 上传

接口的返回值是一个jsonp,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名包裹在JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。简单来说就是他的返回值会被当做参数使用。
window['smCB_1761968139247'] && smCB_1761968139247({
    "code": 1100,
    "detail": {
        "c": 0,
        "len": "12",
        "sign": "Y3aC/fAnw/8PwKLjmNjRGg==",
        "t": 0,
        "timestamp": "1487582755342"
    },
    "requestId": "dfccd993837484d907a4f05e2e1894a5"
})
从web接口进入堆栈打断点,可以发现他进入了一个名为fp.min.js文件。文件样本已经上传。样本下载地方->

fp.txt
(181.86 KB, 下载次数: 0)
2025-11-1 20:10 上传
点击文件名下载附件
下载积分: 吾爱币 -1 CB



2025-11-01-11-42-22-image.png (22.93 KB, 下载次数: 0)
下载附件
2025-11-1 20:02 上传

直接搜索smdata,,总共有4个smdata,全部打下断点。


2025-11-01-13-51-14-image.png (38.97 KB, 下载次数: 0)
下载附件
2025-11-1 20:02 上传

点击登录事件产生之后,断点断住了,_0x1df973就是我们接口的名字


2025-11-01-13-54-55-image.png (36.87 KB, 下载次数: 0)
下载附件
2025-11-1 20:02 上传

控制台打印一直下smdata的值,就是加密之后的参数


2025-11-01-13-58-29-image.png (27.42 KB, 下载次数: 0)
下载附件
2025-11-1 20:02 上传

那么分析一下这个函数,_0x3f1709[_0x2b6eee(0x334)]就是t 函数接收三个参数并调用第一个参数作为函数,_0xff5bfd是加密函数,第二个参数是加密的第一个传参,而_0x1df0c0就是明文
/**
* RJbtt 函数接收三个参数并调用第一个参数作为函数。
*
* @Param {Function} _0x11c620 - 一个函数,后续会被调用。
* @param {*} _0x487263 - 传递给 _0x11c620 的第一个参数。
* @param {*} _0x10e17f - 传递给 _0x11c620 的第二个参数。
* @returns {*} - 返回调用 _0x11c620 函数的结果。
*/
function RJbtt(_0x11c620, _0x487263, _0x10e17f) {
    // 调用传入的函数 _0x11c620,并将 _0x487263 和 _0x10e17f 作为参数传入,
    // 将其返回值返回。
    return _0x11c620(_0x487263, _0x10e17f);
}
_0xff5bfd是加密函数
var smdata =  RJbtt(_0xff5bfd, _0x1ecefa['sign'], _0x1df0c0)
进入_0xff5bfd里面下断点,第一个参数是null,第二个参数是明文。


2025-11-01-14-23-09-image.png (32.11 KB, 下载次数: 0)
下载附件
2025-11-1 20:03 上传

放开断点,可以看到第二次经过这个函数,第一个sign不再是null,而且明文也不一样。那么是为什么呢?还记得第一篇开头我说的那个坑吗,笔者这套加密扣代码搞不出来(还是太菜了),走的补环境。这篇文章笔者也不会去讲如何补环境过这个加密(我也不常用补环境学的也不行),而这个加密函数补环境真的只要学过一点都能过,笔者只讲明文生成跟模拟。这套加密函数走补环境的唯一一个坑呢就是那个jsonp,第一次加密走的是web接口,而第二次加密走的就是校验接口了,而校验接口会取第一次加密的返回值那个sign参数还有时间戳跟长度放到这套加密函数去使用,所以走补环境的话需要把那个jsonp当作参数传进来代码使用,或者直接修改代码变量。笔者走的就是第二种方法。


2025-11-01-14-23-31-image.png (32.11 KB, 下载次数: 0)
下载附件
2025-11-1 20:03 上传

web接口的smdata参数,明文跟加密值:
放到文件对比工具里是一致的。


2025-11-01-14-34-04-image.png (38.57 KB, 下载次数: 0)
下载附件
2025-11-1 20:03 上传



2025-11-01-14-34-38-image.png (84.3 KB, 下载次数: 0)
下载附件
2025-11-1 20:03 上传

verify接口的smDeviceId参数,明文跟加密值:
放到文件对比工具里是一致的。


2025-11-01-14-37-10-image.png (16.36 KB, 下载次数: 0)
下载附件
2025-11-1 20:04 上传



2025-11-01-14-37-51-image.png (34.35 KB, 下载次数: 0)
下载附件
2025-11-1 20:04 上传

verify接口的smDeviceId参数跟web接口的smdata参数走的都是一套加密,只是传的明文不一样。那么接下来就分析明文生成!
明文分析
web接口明文:
我将明文字符串转换成了字典键值对格式,这样子好分析有哪些参数
{
    "channel": "",
    "deviceId": "20251030090855f727de70d51421ed652fc42241a2556e009fd401c01df9d90",
    "plugins": "-",
    "ua": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Mobile Safari/537.36",
    "appVer": "5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Mobile Safari/537.36",
    "lang": "zh-CN",
    "userLang": "undefined",
    "browserLang": "undefined",
    "systemLang": "undefined",
    "langs": "zh-CN",
    "canvas": "32164f7c",
    "timezone": "-480",
    "time": "1",
    "platform": "Win32",
    "url": "登录接口取的url",
    "referer": "登录接口取的referer",
    "res": "400_259_24_2",
    "status": "0000",
    "clientSize": "0_0_400_259_400_259_400_259",
    "appCodeName": "Mozilla",
    "appName": "Netscape",
    "oscpu": "",
    "area": "-1_-1",
    "sid": "1761979615828-4703920",
    "version": "2.0.0",
    "subVersion": "2.0.6",
    "cdp": "0",
    "maxTouchPoints": "0",
    "connectionRtt": "400",
    "cpucount": "2",
    "behavior": "mousemove="
}
全局搜索channel,很容易就找到了生成点,接下来逐一分析所有明文。


2025-11-01-14-55-22-image.png (44.15 KB, 下载次数: 0)
下载附件
2025-11-1 20:04 上传

channel:
channel固定是字符串""
deviceId:
设备唯一标识
这个参数就很有意思,因为他不好找到生成点,必须要去掉缓存才能找到生成点,emmm,大家可以怎么理解这个参数的作用呢,验证码那篇验证码register有个captchaUuid,作用是一致的。就是绑定数据包,将所有明文绑定设备码告诉后端,我这个设备码的环境是啥,然后还有个校验接口的smDeviceId的明文也有这个deviceId,就是将这两个数据包绑定在一起,告诉服务器那边我这个设备码是什么环境干了什么。接下来就看分析流程吧!
deiviceId是由_0x297aae赋值的。


2025-11-01-15-06-02-image.png (39.46 KB, 下载次数: 0)
下载附件
2025-11-1 20:04 上传

搜索_0x297aae,有混淆,扣到本地解混淆分析一下吧


2025-11-01-15-08-05-image.png (26.09 KB, 下载次数: 0)
下载附件
2025-11-1 20:05 上传

通过逻辑或操作获取设备 ID (deviceId),如果执行_0x29275f函数没有获取参数就执行
_0x426e6a函数,_0x426e6a函数也会去执行_0x29275f函数,时间戳跟deviceId都会从_0x21da97对象里面取,_0x21da97对象执行到这个函数的时候已经生成了。而目前未知的就是_0x29275f函数跟_0x21da97的生成。逐一分析吧,先分析或运算之前的_0x29275f函数再去分析_0x21da97对象生成点就能找到对应的生成点了。后续函数太复杂了,就讲讲思路吧,就是他先去缓存里面取smidV2,从local,cookie等等地方取这个值,如果取到了就使用,没取到就生成。笔者这关已经打通了,慢慢磨一段时间大家都能通关的,
_0x21da97 = {
    "key": "SMshumei",
    "deviceId": "20251101155436a238a0b12a288e755236fe9f17b5094d0097629710ac82d90",
    "timestamp": "1487577677129"
}
// 定义一个名为 uEmpx 的函数,接受一个参数 _0x3c9b98。
function uEmpx(_0x3c9b98) {
    // 调用传入的函数 _0x3c9b98,并返回其执行结果。
    return _0x3c9b98();
}
// 定义一个名为 _0x426e6a 的函数,无参数。
function _0x426e6a() {
    // 调用 uEmpx 函数,传入 _0x29275f 作为参数,如果返回结果为 falsy 值,则使用 _0x21da97 对象中的 'deviceId' 属性。
    var _0x2cfa92 = uEmpx(_0x29275f) || _0x21da97['deviceId'];
    // 从 _0x21da97 对象中获取 'timestamp' 属性。
    var _0x4375f9 = _0x21da97['timestamp'];
    // 返回一个对象,包含 deviceId、sign 和 timestamp 三个属性。
    return {
        'deviceId': _0x2cfa92,
        'sign': _0x55806a, // 猜测是web接口返回的sign
        'timestamp': _0x4375f9
    };
}
// 定义一个名为 bYViz 的函数,接受一个参数 _0x55c742。
function bYViz(_0x55c742) {
    // 调用传入的函数 _0x55c742,并返回其执行结果。
    return _0x55c742();
}
// 定义一个名为 OQzCs 的函数,接受一个参数 _0x5abbac。
function OQzCs(_0x5abbac) {
    // 调用传入的函数 _0x5abbac,并返回其执行结果。
    return _0x5abbac();
}
// 定义一个名为 rscJU 的函数,接受一个参数 _0x1bfedc。
function rscJU(_0x1bfedc) {
    // 调用 OQzCs 函数,传入 _0x1bfedc 作为参数,并返回其结果。
    return OQzCs(_0x1bfedc);
}
// 进行一个逻辑或操作,首先调用 bYViz 函数,传入 _0x29275f 作为参数,如果返回结果为空,则调用 rscJU 函数,传入 _0x426e6a,获取其 deviceId。
bYViz(_0x29275f) || rscJU(_0x426e6a)['deviceId'];
plugins:
plugins是浏览器插件,因为笔者用的是指纹浏览器,所以是-字符串,拿个真实浏览器的值给大家看看。
"ChromePDFViewerPortableDocumentFormatinternal-pdf-viewer2,ChromiumPDFViewerPortableDocumentFormatinternal-pdf-viewer2,MicrosoftEdgePDFViewerPortableDocumentFormatinternal-pdf-viewer2,PDFViewerPortableDocumentFormatinternal-pdf-viewer2,WebKitbuilt-inPDFPortableDocumentFormatinternal-pdf-viewer2",
这个值的生成也很简单,就是获取浏览器PDF插件进行拼接,代码混淆了,扣下来分析


2025-11-01-17-35-07-image.png (38.48 KB, 下载次数: 0)
下载附件
2025-11-1 20:05 上传

分析之后就是执行了_0x2724df函数
function MBwzJ(_0x3c9b98) {
            return _0x3c9b98();
        }
MBwzJ(_0x2724df)
进入_0x2724df函数分析


2025-11-01-17-38-20-image.png (30.43 KB, 下载次数: 0)
下载附件
2025-11-1 20:05 上传

扣代码到本地分析,其函数就是从浏览器获取所有PDF插件进行-拼接,如果没有就是返回-
// 定义一个空对象 _0x2618f6
var _0x2618f6 = {};
// 在 _0x2618f6 对象中添加一个名为 'plugins' 的属性,它的值是一个对象。
_0x2618f6['plugins'] = {
    // 插件的详细信息以对象的形式存储,每个插件都有一个唯一的索引
    "0": {
       'description': "Portable Document Format", // 插件描述
       "filename": "internal-pdf-viewer",        // 插件文件名
       "length": 2,                               // 插件长度
       "name": "PDF Viewer"                       // 插件名称
    },
    "1": {
       'description': "Portable Document Format",
       "filename": "internal-pdf-viewer",
       "length": 2,
       "name": "Chrome PDF Viewer"
    },
    "2": {
       'description': "Portable Document Format",
       "filename": "internal-pdf-viewer",
       "length": 2,
       "name": "Microsoft Edge PDF Viewer"
    },
    "3": {
        'description': "Portable Document Format",
        "filename": "internal-pdf-viewer",
        "length": 2,
        "name": "WebKit built-in PDF"
    },
    "4": {
        'description': "Portable Document Format",
        "filename": "internal-pdf-viewer",
        "length": 2,
        "name": "Chromium PDF Viewer"
    },
    // plugins 对象的总长度
    length: 5
};
// 定义一个名为 amKjO 的函数,接受两个参数 _0x15736f 和 _0x3d468c。
function amKjO(_0x15736f, _0x3d468c) {
    // 返回两个参数的和(简单的字符串拼接)
    return _0x15736f + _0x3d468c;
}
// 定义一个名为 _0x2724df 的函数,无参数。
function _0x2724df() {
    // 初始化一个空数组 _0x1a645e 和一个空字符串 _0x2ba3fe
    var _0x1a645e = [],
        _0x2ba3fe = '';
    // 循环遍历 _0x2618f6['plugins'] 对象中的每个插件
    for (var _0x3b2745 = 0; _0x3b2745
ua跟appVer这两个就很好模拟了,直接用py模拟
android_versions = [
        "6.0", "7.0", "7.1.1", "8.0", "9",
        "10", "11", "12", "13"
    ]
    # 生成设备列表
    devices = [
        "Nexus 5", "Pixel 4", "Pixel 7",
        "Samsung Galaxy S10", "Xiaomi Mi 9",
        "OPPO R15"
    ]
    # 生成构建号列表
    builds = [
        "MRA58N", "QQ3A.200805.001",
        "RQ3A.210705.001", "QP1A.190711.020.C3",
        "RKQ1.211001.001"
    ]
    # 生成 Chrome 版本和 Edge 版本
    chrome_versions = [f"{random.randint(90, 141)}.0.{random.randint(1000, 9999)}.0" for _ in range(20)]
    edge_versions = [f"{random.randint(90, 141)}.0.0.0" for _ in range(20)]
    # 扩展各个列表以确保有足够的条目
    android_versions_expanded = [f"{version}.{random.randint(0, 999)}" for version in android_versions for _ in range(5)]
    devices_expanded = [f"{device} {random.randint(1, 10)}" for device in devices for _ in range(5)]
    builds_expanded = [f"{build}.{random.randint(1, 999)}" for build in builds for _ in range(5)]
    # 确保每个扩展列表都有至少 20 个条目
    android_versions_expanded = random.sample(android_versions_expanded, min(20, len(android_versions_expanded)))
    devices_expanded = random.sample(devices_expanded, min(20, len(devices_expanded)))
    builds_expanded = random.sample(builds_expanded, min(20, len(builds_expanded)))
    # 生成 1000 个用户代理
    user_agents = []
    def random_ua_appver():
        android = random.choice(android_versions_expanded)
        device = random.choice(devices_expanded)
        build = random.choice(builds_expanded)
        chrome = random.choice(chrome_versions)
        edge = random.choice(edge_versions)
        ua = f"Mozilla/5.0 (Linux; Android {android}; {device} Build/{build}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{chrome} Mobile Safari/537.36 Edg/{edge}"
        appVer = ua.replace('Mozilla/', '')
        return {"ua": ua, "appVer": appVer}
    ]
lang,userLang,browserLang,systemLang,langs,timezone,time,platform,url,referer,status,appCodeName,appName,oscpu,version,subVersion,cdp,maxTouchPoints,cpucount,都是固定的,但是都是从浏览器获取的,也可以自己模拟,校验的不严格,别问笔者为什么知道,都是在指纹浏览器测了几天测出来的。
lang:当前网页或文档的语言设置。
userLang:用户在浏览器或操作系统中设置的首选语言。
browserLang:浏览器检测到的用户界面的语言。
systemLang:操作系统的语言设置。
langs:用户代理或浏览器支持的一组语言列表。
timezone:用户所在区域的时区信息。
time:两个函数的时间差,如果断点定的很久就会很长。
platform:用户所使用的操作系统平台或设备类型。
url:当前页面的统一资源定位符(URL)。
referer:用户在访问当前页面之前所访问的页面的URL。
status:HTTP请求的状态码,表示请求的处理结果。
appCodeName:浏览器的应用程序代码名,通常用于识别浏览器。
appName:浏览器的应用程序名称。
oscpu:操作系统的CPU架构信息。
version:应用程序或浏览器的版本号。
subVersion:应用程序或浏览器的子版本号。
cdp:Chrome DevTools协议(Chrome Developer Protocol),用于与Chrome浏览器进行通信。
maxTouchPoints:设备支持的最大触控点数,通常用于触摸屏设备。
cpucount:设备的CPU核心数。
接下来继续其他需要模拟的参数,
clientSize跟res都是取的都是逻辑分辨率,直接py模拟
# 640 × 1136        iPhone 5, 5S, SE (1代)        2        320 × 568        iOS 标准
# 750 × 1334        iPhone 6, 6S, 7, 8        2        375 × 667        iOS 标准
# 1080 × 1920        许多中端安卓手机        ≈3        360 × 640 (估算)        安卓设计常用宽度360dp左右
# 1125 × 2436        iPhone X, XS, 11 Pro        3        375 × 812        iOS 标准
# 1170 × 2532        iPhone 12, 13, 14        3        390 × 844        iOS 标准
# 1440 × 2560        高端安卓手机 (QHD)        ≈4        360 × 640 (估算)        安卓设计常用宽度360dp左右
# 720 × 1280        低端安卓手机 (HD)        ≈2        360 × 640 (估算)        安卓设计常用宽度360dp左右
# 1080 × 2340        许多全面屏手机 (Full HD+)        ≈3        360 × 780 (估算)        安卓设计宽度仍约360dp
logical_resolutions = [
(320, 568),  # iPhone 5, 5S, SE (1代)
(375, 667),  # iPhone 6, 6S, 7, 8
(360, 640),  # 许多中端安卓手机,估算逻辑分辨率
(375, 812),  # iPhone X, XS, 11 Pro
(390, 844),  # iPhone 12, 13, 14
(360, 640),  # 高端安卓手机 (QHD),估算逻辑分辨率
(360, 640),  # 低端安卓手机,估算逻辑分辨率
(360, 780),  # 许多全面屏安卓手机,估算逻辑分辨率
]
width, height = random.choice(logical_resolutions)
res_str = f"{height}_{width}_24_2"
client_size_str = f"0_0_{height}_{width}_{height}_{width}_{height}_{width}"
connectionRtt就是发包时长。
"connectionRtt": str(random.choice([50, 100, 200, 300]))
area鼠标点击的坐标
f"{str(random.randint(-1, 20))}_{str(random.randint(-1, 20))}"
sid代码
// 接受两个参数 _0x10b809 和 _0x5189c7,返回它们的乘积。
function LhcZg(_0x10b809, _0x5189c7) {
    return _0x10b809 * _0x5189c7;
}
// 接受两个参数 _0x283ed8 和 _0x545f8c,返回它们的和。
function KjNxI(_0x283ed8, _0x545f8c) {
    return _0x283ed8 + _0x545f8c;
}
function _0x3dff0b() {
    // 获取当前时间的时间戳(自1970年1月1日以来的毫秒数),并将其转换为数字
    var _0x34f6d9 = +new Date();
    // 生成一个 0 到 100000000 之间的随机数并取整
    var _0xe68f97 = Math['floor'](LhcZg(Math['random'](), 100000000));
    // 返回一个字符串,由当前时间戳和随机数拼接而成,中间用 '-' 分隔
    return KjNxI(KjNxI(_0x34f6d9, '-'), _0xe68f97);
}
console.log(_0x3dff0b());
canvas:
canvas的值就是执行_0x11bd04获取的返回值
function KkwIm(_0xd8006e) {
            return _0xd8006e();
        }
KkwIm(_0x11bd04)


2025-11-01-18-02-06-image.png (31.69 KB, 下载次数: 0)
下载附件
2025-11-1 20:06 上传

_0x11bd04函数,混淆的,扣代码到本地复现


2025-11-01-18-00-44-image.png (39.14 KB, 下载次数: 0)
下载附件
2025-11-1 20:06 上传

function YFboj(_0xf29fb1, _0x1aad90) {
            return _0xf29fb1
代码就是将是因canavs画图取生成的图的值分割取值导出,,估摸着哪个地方会变,我测试的一直没变所以我固定写了,转换成html代码看看究竟画了啥
   
   
   
   
    Canvas 示例
   
        canvas {
            border: 1px solid black; /* 绘制一个边框以便观察 Canvas */
        }
   

   
   





2025-11-01-18-05-06-image.png (14.64 KB, 下载次数: 0)
下载附件
2025-11-1 20:06 上传

轨迹:
笔者是弄的随机坐标
def generate_click_trajectory(num_clicks, x_range, y_range):
    """
    生成随机点击的轨迹
    参数:
    num_clicks (int): 点击的数量
    x_range (tuple): x 坐标范围 (min_x, max_x)
    y_range (tuple): y 坐标范围 (min_y, max_y)
    返回值:
    list: 包含点击点的列表,每个点是一个 (x, y) 元组
    """
    trajectory = []
    for _ in range(num_clicks):
        x = random.randint(x_range[0], x_range[1])  # 随机生成 x 坐标
        y = random.randint(y_range[0], y_range[1])  # 随机生成 y 坐标
        trajectory.append((x, y))  # 将生成的点添加到轨迹中
    return trajectory
num_clicks = random.randint(5,10)
x_range = (0, width)  
y_range = (0, height)  
trajectory = generate_click_trajectory(num_clicks, x_range, y_range)
print(trajectory)
结尾
可以正常上并发了,笔者想说:世上无难事,只要肯登攀。

函数, 参数

MichealGeng   

大佬厉害,感谢分享,最近正要做相关工作,学习了
Soar119   

学习一下,有点复杂
someone52   

很详细,自己经常分析到一半就没方向了!
vislen   

学习下,感谢分享
w1009006652   

网络游戏有用没?????
cslgpl   

学习学习
qte123   

学习一下,有点复杂
woyaokan   

学习一下,还没完全看懂。。
xierq7   

感谢大佬的分享!
您需要登录后才可以回帖 登录 | 立即注册

返回顶部