数美点选验证协议全面剖析

发布时间:2024年01月18日

目标网站

aHR0cHM6Ly9zZWN1cmUuZWxvbmcuY29tL3Bhc3Nwb3J0L2xvZ2luX2NuLmh0bWw/bmV4dHVybD1odHRwczovL3d3dy5lbG9uZy5jb20v

在这里插入图片描述

这一次要分析的是这个数美的点选验证。

分析请求

接下来对这个验证的请求进行抓包分析,看看有哪几个请求是需要逆向的。
在这里插入图片描述

首先是一个register请求,这个对应的是图片的获取
在这里插入图片描述

payload参数如图,其中的mode表示的是验证模式,当前是点选验证,如果model的值是slide,那么说明是滑块验证。

在这里插入图片描述

返回的数据是一张点选的图片,还有order字段的值
在这里插入图片描述

通过preview可以看到,这个order字段就是文字内容。

所以我们接下来要做的事情就是通过调用这个请求,拿到返回的图片和文字数据,然后通过图片识别出文字的位置,找到坐标点,然后通过代码去构造坐标数据。

接着当我们对图片进行点选的时候,会产生这么一个请求
在这里插入图片描述

会发送一个verify请求,如果失败了,会返回REJECT,如果成功的话则是 PASS。
在这里插入图片描述

然后再往上会有一个conf请求,这个请求的payload参数和register提交的参数是一样的,所以这俩请求分析一个就可以了。

那么需要分析的核心的请求就只有下面三个:

https://captcha1.fengkongcloud.cn/ca/v2/fverify
https://captcha1.fengkongcloud.cn/ca/v1/register
https://captcha1.fengkongcloud.cn/ca/v1/conf

动态JS无法调试

在分析之前我们需要先解决一个问题,
在这里插入图片描述

这个conf请求调用栈的JS文件,是一个动态变化的,每次url都不一样,这样就导致我们的断点在下断以后再次断下。

这个问题的解决方案在于将加载JS时的url替换为固定的,可以通过charles或者mimproxy来解决,这两个都可以作为代理拦截http请求。
在这里插入图片描述

在Charles中捕获这个请求,然后右键->SaveResponse,把页面源码保存下来。
在这里插入图片描述

打开Charles的Map Local功能

在这里插入图片描述

添加一个Mapping,设置了这个以后,当有网络请求访问了我们指定的这个map页面,就会被替换为本地保存的html文件,这样就可以保证JS一直是固定的了。

也可以用mitmproxy写一个拦截脚本来处理这个问题,代码如下:

from mitmproxy import http
from mitmproxy.http import Request


def request(flow):
    if flow.url.startswith("https://secure.elong.com/passport/login_cn.html?nexturl=https://www.elong.com/"):
        with open("res.html",mode="rb",encoding="utf-8") as f:
            content=f.read()

        flow.response = Response.make(
            200,
            content,
            {"Content-Type":"text/html"}
        )


def response(flow: http.HTTPFlow):
    pass

不过还是Charles方便,直接配置好就可以了。

代码混淆处理

然后我们来解决代码混淆的问题。
在这里插入图片描述

接着抓一个包,找到conf这个请求,这个请求指向的是api.js文件,也就是我们刚刚替换的那个动态的JS
在这里插入图片描述

点进去发现,所有的代码都是经过混淆的。

var _0x19e1cf = _0x136e2f[_0x2ae8e9(0x2fd)]

这种混淆实际上就是把字符串,替换成了函数调用,用来干扰分析。我们把这个JS文件保存下来,做一个整体的分析
在这里插入图片描述

一共有四个大函数,其中两个是自执行函数。这个页面的混淆代码大部分在第三个自执行函数里面,其他三个函数代码量都比较小。
在这里插入图片描述

其中大部分的混淆代码都是在执行_0x1f0d这个函数。

那么我们就可以把除了第三个大函数以外的函数全部抠下来,拿到本地,去执行一下加密的函数,看能不能得到结果
在这里插入图片描述

经过实际测试,确实是可以正常运行,并且打印出对应的字符串。

console.log(_0x1f0d(process.argv[2]))

然后我们把这个混淆函数的参数替换为命令行参数,从而使用python来调用,来达到批量去混淆的目录,python代码如下:

import subprocess
import re

def Decode(hex_rg):
    res= subprocess.check_output(f"node main.js {hex_rg}",shell=True)
    res_string=res.decode("utf-8").strip()
    return res_string


def run():
    with open("f1.js", mode="r", encoding="utf-8") as fr, open("f2.js", mode="w", encoding="utf-8") as fw:
        for line in fr:
            match_list = re.findall("(_0x425d8a\((.*?)\))", line)
            if not match_list:
                fw.write(line)
                continue

            for func_string,hex_str in match_list:
                line=line.replace(func_string,f'"{Decode(hex_str)}"')
            fw.write(line)


if __name__ == '__main__':
    run()

这个脚本做的事情就是打开f1.js,读取里面的内容,通过正则匹配的方式筛选出符合要求的代码,通过调用nodejs解混淆脚本得到结果,对其进行批量替换。

等遇到需要重点分析的代码,可以用这个方式可以去除部分混淆,提高一些代码可读性,帮助分析代码。这个脚本在后面的分析里面可以帮我们节省很多时间。

conf请求分析

我们先来分析第一个conf请求,里面携带了这么几个参数

appId: default
organization: xQsKB7v2qSFLFxnvmjdO
callback: sm_1705412287345
sdkver: 1.1.3
model: select
captchaUuid: 20240116213802Zwas5htESARemRJWfW
rversion: 1.0.4
lang: zh-cn
channel: DEFAULT

organization是一个ID,这个是固定的,多测几次就会发现,而callback是一个时间戳,那么对于conf这个请求,我们只需要去分析captchaUuid这个字段就可以了。
在这里插入图片描述

找到conf请求的调用堆栈,在中间的一个位置打一个断点,反正都是混淆的,在哪都没区别,只要在附近找到了需要跟踪的字段,一直往上找就行了。
在这里插入图片描述

断下以后,当前的作用域里面,并没有我们需要的参数,所以需要沿着调用栈一直往上翻
在这里插入图片描述

翻到这个调用栈的时候,终于找到了我们需要的相关参数

'captchaUuid': _0x2049e1

所以我们现在就要往上找_0x2049e1里面的值是从哪来的

在这里插入图片描述

_0x2049e1在这个位置被赋值,那我们接下来就要分析这个代码。

_0x2049e1 = userConfig[_0x4c37ed(0x3be)] || _0x332a8d[_0x243c3a[_0x4c37ed(0x483)]][_0x4c37ed(0x4f4)]();

这个代码实际上就是一个函数调用
在这里插入图片描述

这里可以对这个位置的代码进行选中,然后跳转到相应的代码页面
在这里插入图片描述

就跳到了这个位置,代码如下:

'getCaptchaUuid': function _0x2d350e() {
                var _0x49a2b0 = _0x247065
                  , _0x1163e8 = ''
                  , _0x40ad39 = _0x49a2b0(0x42a)
                  , _0x3531f2 = _0x40ad39['length'];
                for (var _0x235133 = -0x149e + -0x231d * 0x1 + -0x37bb * -0x1; _0x235133 < -0x2214 + 0xd * 0x223 + 0xe9 * 0x7; _0x235133++) {
                    _0x1163e8 += _0x40ad39['charAt'](Math[_0x49a2b0(0x2c6)](Math[_0x49a2b0(0x134)]() * _0x3531f2));
                }
                return _0x529f28[_0x49a2b0(0x294)](this[_0x49a2b0(0x232)](), _0x1163e8);
            },

这个代码的分析就没有什么技术含量了,一行一行硬看,反正也没有多少代码

captchaUuid: 20240116213802Zwas5htESARemRJWfW

参考这个captchaUuid的值,一边调试一边分析,我这里直接说结论

captchaUuid=当前时间+17个随机字符

实现代码如下:

def gen_captcha_uuid():
    total_string = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678"
    part = "".join([random.choice(total_string) for i in range(18)])
    ctime = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    captcha_uuid = f"{ctime}{part}"
    return captcha_uuid

这样的话这个请求我们就搞定了。

分析fverify请求

整体代码分析

fverify请求携带的参数如下:

en: y+ugz9NIWys=
dy: Rfpr5oqb5y4=
xy: YabT6nmJOC0=
tb: 3jSn4gNaAVM=
mu: 9Zlg08y1MpaONqmLgK0lCn6u9wurdUqB5gECmblJXlSmnXHCWXXjiqvWSDI1PvzKgJdsNe46/hqf7zZh2MbjiPprUUXfMXi5+mLlFmVm6mrUS0NZ6OhMKZzNznVo51GI44gF1jVQF7Dh4/k62Zo93u4USF1RoqZU4LXyLyhHZMdhlk5nk5/NUMITc7kJqy7ngnBWIDPhl72rZtFxlvPKXxMXZnJU+GwA5pnf4/41T4rM6rYRV9Xr2E9XCLBhKno3eQAtSxt7wlDl6GMcg3LJ81dZGD6UVwy1DJJuG6fMv/M=
oc: h9oFKi8cHpg=
mp: WYfkIZp7GoA=
nu: C0kH/bWLjw8=
qd: adKA4Y0ncgGB4U6j47xchBZw9rO2THeeS/Z7vaOjAeKvmR57DOi6wsSJgovJUg2YQGQgooGYYrd2BhXCCkVQeY2gk1w7g2IquFeRYOlajtwqm5aZTx8FBx6ASb9VM7LNiZc0cVEnpnCrnXt0ICRagsT4sIYxGFg+EPenpHhwldu1Ufb8VNYIIrYBESmTOhpdttfd8gUBE36NGzqeXQpvRiqf4JT6eWB73TzFVEtzcjUHskwpvgqSmTYoQwG/gjy5APzSd96aTgTzKeX8I/wdupwYJnWvjmWWJ2R4JprB45o=
ww: aOGVECVeH60=
kq: mtlOTdT5LOE=
jo: lQ90183KgD4=


固定:
ostype: web
sdkver: 1.1.3
protocol: 180
rversion: 1.0.4
organization: xQsKB7v2qSFLFxnvmjdO
act.os: web_pc

已分析
captchaUuid: 20240117123606cSbtzkfecDkBDbC7Sr
callback: sm_1705466200576


register接口的返回值
rid: 202401171236155825d945349adfd458

这个请求里面,排除掉不需要分析的部分,剩下需要分析的字段一共有12个。
在这里插入图片描述

查看请求的调用栈,在最后两个调用栈下断点
在这里插入图片描述

断下之后来分析下当前的这一行代码

this[_0x2c1c24(0x1b5)](_0x5c100a, _0x599170, _0x1ed2cb, _0x28800d, _0x5c6636, _0x4d541b);

为了方便阅读,我把这个代码进行简化

this[""sendRequest""]('https://', "captcha1.fengkongcloud.cn", "/ca/v2/fverify", _0x28800d, _0x5c6636, _0x4d541b);

这个位置实际上是在发送https请求,前面三个参数是在拼接请求的域名
在这里插入图片描述

而第四个参数是一个对象,里面包含的内容就是我们需要跟踪的字段。那么我们现在就摇追踪_0x28800d的来源,看这个数据是怎么生成的。
在这里插入图片描述

我们先把这一段代码的混淆给处理一下,用之前解混淆的那个方法。

var 0x2d6657 = {
        'nyzWi': "mouseMoveX",
        'SZdlb': _0x6f9c3c["pzOLX"],
        'TGQiS': function (_0x569f5d, _0x276cc4) {
            var _0x5d1724 = _0x2c1c24;
            return _0x6f9c3c["onIeC"](_0x569f5d, _0x276cc4);
        }
    };

var _0x1fe1cf,
    _0x44bcff = this["_config"],
    _0x599170 = _0x44bcff['domains'],
    _0x3d6fed = _0x44bcff["fVerifyUrlV2"],
    _0x1ed2cb = _0x3d6fed === undefined ? _0x3b4628 : _0x3d6fed,
    _0x49bb92 = _0x44bcff["organization"],
    _0x20df23 = _0x44bcff["appId"],
    _0x4143cf = _0x44bcff["channel"],
    _0x435e2e = _0x44bcff['VERSION'],
    _0x7306d7 = _0x44bcff['lang'],
    _0x294474 = _0x44bcff["SDKVER"],
    _0x1b27c8 = _0x44bcff["_successCallback"],
    _0x2d5257 = _0x44bcff["mode"],
    _0x5b58d1 = this['_data'],
    _0x536387 = _0x5b58d1["errMsg"],
    _0x74fdb5 = _0x5b58d1["trueWidth"],
    _0x31e834 = _0x6f9c3c["tIGQt"](_0x74fdb5, undefined) ? -0x1338 + -0x145f + -0x7eb * -0x5 : _0x74fdb5,
    _0x3717b1 = this["getRegisterData"](_0x6f9c3c["ZGnpS"]), _0x298b01 = this["getMouseAction"](),
    _0x528bd = _0x6f9c3c["VQpVP"], _0x49f479 = this["getSafeParams"](),
    _0x28800d = _0x2460cd[_0x6f9c3c["Nlbsb"]]["extend"]((_0x1fe1cf = {
        'organization': _0x49bb92
    },
        
            _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, 'mp', this['getEncryptContent'](_0x20df23, _0x6f9c3c["NnjXa"])),
            _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, 'oc', this["getEncryptContent"](_0x4143cf, "c2659527")),
            _0x1f0d12['default'])(_0x1fe1cf, 'xy', this["getEncryptContent"](_0x7306d7, _0x6f9c3c["wIUSM"])),
            _0x1f0d12["default"])(_0x1fe1cf, 'jo', this["getEncryptContent"](_0x49f479, _0x6f9c3c["HwXkn"])),
            _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, _0x6f9c3c["ZGnpS"], _0x3717b1),
            _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, _0x6f9c3c["CJfJJ"], _0x435e2e),
            _0x1f0d12[_0x6f9c3c['Nlbsb']])(_0x1fe1cf, _0x6f9c3c["WWhmm"], _0x294474),
            _0x1f0d12[_0x6f9c3c['Nlbsb']])(_0x1fe1cf, _0x6f9c3c["VRUfH"], "180"),
            _0x1f0d12[_0x6f9c3c["Nlbsb"]])(_0x1fe1cf, "ostype", _0x528bd),
        _0x1fe1cf), _0x298b01)

_0x2460cd["default"]["log"](_0x119bdb["LOG_ACTION"]["SEND_VERIFY"]),
    this["sendRequest"](_0x5c100a, _0x599170, _0x1ed2cb, _0x28800d, _0x5c6636, _0x4d541b);

当然也可以不处理,直接调试,可能你有解混淆的功夫,人家代码都已经抠完了。
在这里插入图片描述

这一段代码在做的事情首先是定义了一个organization的字典,然后往这个字典里面进行赋值;而_0x298b01这个也是一个字典,然后再通过extend操作对两个字典进行合并。

在这里插入图片描述

而这四个字段的值,都是通过调用加密函数getEncryptContent,然后传入两个参数,来获取到的参数,所以我们可以先对这四个字段进行分析。
在这里插入图片描述

然后再对这个函数进行化简,我们要知道传入的参数是什么。化简也没什么好方法,就是一个个的手动替换。

xy: YabT6nmJOC0=
oc: h9oFKi8cHpg=
mp: WYfkIZp7GoA=
jo: lQ90183KgD4=

既然这个四个字段传入的参数是固定的,那么返回的值肯定也是固定的,所以这几个参数我们可以直接写死了。

getEncryptContent函数分析

接着来分析这个加密函数

在这里插入图片描述

把鼠标选中内容,然后就可以跳转到对应的位置
在这里插入图片描述

就可以定位到加密函数的位置

在这里插入图片描述

然后用手动挡去混淆的方式,可以看到大致的一些信息,盲猜是一个DES和base64加密。

'mp',this['getEncryptContent']('default', '9cc268c1'),
'oc',this["getEncryptContent"]('DEFAULT', "c2659527")),
'xy',this["getEncryptContent"]('zh-cn', 'b1807581')),
'jo',this["getEncryptContent"]('10', '6d005958')),

那么这里就可以做一个大胆的尝试,用python算法实现一个DES和base64加密,把第一个参数当作是需要加密的字符串,第二个参数当作是Key,看能否输出对应的结果

实现代码如下:

if __name__ == '__main__':
    key = b'9cc268c1'
    data_string = 'default'

    pad_func = lambda text: text + '\0' * (DES.block_size - (len(text.encode('utf-8')) % DES.block_size))
    aes = DES.new(key, DES.MODE_ECB)
    enc_data = aes.encrypt(pad_func(data_string).encode("utf8"))
    res = base64.b64encode(enc_data).decode('utf-8')
    print(res)

然后查看运行的结果
在这里插入图片描述

mp: WYfkIZp7GoA=

和我们分析的请求中的mp结果是完全一致的。这种方式有些取巧,一部分情况可能不太好使,所以我们还有第二种方式,直接扣代码,大力出奇迹。
在这里插入图片描述

把鼠标放在这个位置,直接跳转到DES源码

在这里插入图片描述

跳转到这个位置之后 把这个函数还有Base64加密的函数全部扣下来,然后运行,缺什么扣什么,一直扣到不报错为止。
在这里插入图片描述

在这里插入图片描述

这样也可以拿到同样的结果

分析其他参数

在这里插入图片描述

前面四个通过DES加密的参数我们已经分析完了,接下来需要分析这个_0x298b01里面的参数来源。
在这里插入图片描述

通过处理过的JS代码可以找到来源,_0x298b01来自于this["getMouseAction"]()的函数结果。通过函数名字getMouseAction大概可以猜到这个对象的数据应该是记录的一些鼠标的坐标信息。
在这里插入图片描述

然后跳转到函数代码的位置,我们把这段代码使用解混淆的脚本进行处理。
在这里插入图片描述

重点关注case "spatial_select"里面的代码,这个里面就是我们所需要的参数,这个switch里面对应的应该是各个不同的验证分支。

spatial_select对应的是点选,slide对应的是滑块。

在这里插入图片描述

然后函数结束的地方还有其他的一些返回数据

_0x4639e5['qd'] = this["getEncryptContent"](_0x23de95, '3c9ed5cb'),

_0x4639e5['mu'] = this['getEncryptContent'](_0x3602ea, "e7e1eb0d"),

_0x4639e5['ww'] = this["getEncryptContent"](_0x6f9c3c["pxDrO"](_0x53f1f2, _0x5caf5a), '17a94a08'),

_0x4639e5['nu'] = this['getEncryptContent'](_0x4c1632, "390aac0d"),

_0x4639e5['dy'] = this["getEncryptContent"](_0x1a7546, "a9001672"),

_0x4639e5["act.os"] = _0x46483a;

_0x4639e5['tb'] = this["getEncryptContent"](_0x2460cd[_0x6f9c3c["Nlbsb"]]["__userConf"]["console"], '6f5e9847'),
    
_0x4639e5['en'] = this["getEncryptContent"](_0x2460cd[_0x6f9c3c["Nlbsb"]]["runBotDetection"](), "9fc1337f"),

_0x4639e5['kq'] = this["getEncryptContent"](-(0x7 * -0x537 + -0x1f77 + 0x1 * 0x43f9), _0x6f9c3c['SGQFW'])

这些参数都是通过getEncryptContent这个函数进行加密的,那么我们只需要搞清楚传入的参数分别是什么数据,就可以对整个请求进行模拟了。

在这里插入图片描述

第一个参数和第二个参数是一样的,是一个四个成员的数组,这个就对应了我们进行点选的坐标。

在这里插入图片描述

而另外三个参数则是固定值

在这里插入图片描述

后两个参数是整张点选图片的宽度和高度,可以直接看下_4fc323这个对象

在这里插入图片描述

里面是这个图片对象的信息,包括图片的宽度和高度,然后把剩下的参数也处理一下

_0x4639e5['qd'] = this["getEncryptContent"](_0x23de95, '3c9ed5cb'),
_0x4639e5['mu'] = this['getEncryptContent'](_0x3602ea, "e7e1eb0d"),
 _0x4639e5['ww'] = this["getEncryptContent"](28504615, '17a94a08'),
 _0x4639e5['nu'] = this['getEncryptContent'](300, "390aac0d"),
 _0x4639e5['dy'] = this["getEncryptContent"](150, "a9001672"),
 _0x4639e5["act.os"] = _0x46483a;
_0x4639e5['tb'] = this["getEncryptContent"](1, '6f5e9847'),
_0x4639e5['en'] = this["getEncryptContent"](0, "9fc1337f"),
_0x4639e5['kq'] = this["getEncryptContent"](-1, 'ebee8dcc')

这一部分的字段,除去_0x23de95是需要分析的坐标信息外,其他的都可以通过传参的方式,调用getEncryptContent函数来获取到对应的值。

分析坐标算法

接下来我们需要对剩下的两个坐标信息的参数进行分析
在这里插入图片描述

其中,_0x23de95selectData_0x3602eamouseData,这两个数据全部都来自于this['data']

在这里插入图片描述

这里我们通过调用栈,找到最上一层的堆栈

在这里插入图片描述

定位到这个位置发现,this['_data']['mouseData']_0x226da4赋值了,所以我们继续跟踪_0x226da4的值。

正常来说switch case分支里面的代码不会被依次执行,但是这个函数不太一样,外面套了一层while循环,然后在switch的变量是一个数组,里面的值是2,0,3,4,1,所以这个switch分支会按照20341的顺序去执行。

在这里插入图片描述

接下来需要对这整个函数进行分析,还是先用脚本去除部分的混淆

在这里插入图片描述

来分析去除混淆后的代码,这样比较方便,在54行这里通过push操作往selectData里面添加数据,那么我们只需要看被添加的数据是在哪生成的

在这里插入图片描述

_0x27bb65在这里被赋值,而这个就是我们要的坐标

_0x27bb65 = [
    _0x42ca39,
    _0x1d0195["IXgiz"](_0x28ed43['y'], _0x1e9886) / _0x30fbb2,
    _0x52aa32
];

这个坐标信息实际上可以拆解为列表中的三个元素,通过逗号分割开,第一个和第三个元素是一个坐标数据,中间的元素是一个算法。我们需要搞清楚这三个数组成员分别是什么。
在这里插入图片描述

第三个成员_0x52aa32实际上就是一个字符串格式的时间戳,前两个应该是当前的坐标通过算法计算出来的结果。

在这里插入图片描述

第一个值的调用来源于上面一行代码

_0x42ca39 = _0x1d0195["rlnuS"](_0x28ed43['x'] - _0x5a1fbf, _0xd3e4df);

这个是一个函数调用,传递了两个参数,分别查看一下这几个数据是什么

在这里插入图片描述

第一个参数是当前鼠标的X坐标,这个坐标可能和真实的坐标不一样,可能是做了等比例缩放,后续可以通过算法的方式来构造。

第二个参数是图片的宽度,然后再来看一下函数的原型:

'rlnuS': function(_0x4781a3, _0x52d7dc) {
     return _0x4781a3 / _0x52d7dc;

这个函数的代码很简单,就是一个除法指令。

_0x1d0195["IXgiz"](_0x28ed43['y'], _0x1e9886) / _0x30fbb2

至于第二个值,是当前的y坐标,跟第一个值的算法几乎没啥区别。到这里,我们的坐标算法就分析完成了。

结束

最后,如果想要把整个接口进行自动化,需要做这么几个事情。首先把上面的分析过程整理成代码,把每个表单提交的数据对应上,

然后需要对接打码平台,获取到点选验证的图片以后,通过打码平台的接口拿到坐标信息,通过提交坐标信息等数据通过点选验证的校验接口。这样就可以完成这个网站的自动化登陆了。我对这个过程并没有兴趣,只研究点选验证的原理以及逆向分析思路,各位有兴趣可以自行尝试。

文章来源:https://blog.csdn.net/qq_38474570/article/details/135671079
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。