吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 15505|回复: 30
上一主题 下一主题
收起左侧

[Web逆向] 数某风控JS算法分析

  [复制链接]
跳转到指定楼层
楼主
AnonymousQsh 发表于 2020-2-13 22:22 回帖奖励
本帖最后由 AnonymousQsh 于 2020-2-13 22:22 编辑

近来闲来无事, 然后打算分析一下数某风控中的js算法, 来提升一下自己的能力.

样本来源: 某鱼登录页面fvp2.js 这个直接获取可能会变, 我将我当时分析的样本上传到附件.

拿到代码, 我们可以发现这个代码是被混淆过的, 因此我们需要先简单处理一下, 便于我们阅读, 这里我直接找的一个js代码反混淆的网站(prettifyjs), 这里会对hex字符串进行转码, 阅读起来会容易一点.

代码可以分为4部分, 接下来将对每一部分分别讲解作用, 悄悄的说一句, 重点都在第四部分上

第一部分: 变量名称存储数组

这里存储了一些在函数中用到的变量和字符串, 这个数组函数挺多的, 在这里我就不都贴上来了.

var _0x9beb = ['cG9UbmVlcmNz', 'eWRvYg==', ..., 'WW5lZXJjU3Jlbm5Jem9t'];

第二部分 数组处理函数


/**
 * params _0x314a53: 上面的字符串数组
 * params _0x280ed8: 计数个数
 * 把前 _0x280ed8 +1 个元素放到数组末尾
 */
(function (_0x314a53, _0x280ed8)
{
    var _0x1f958f = function (_0x59c3ce)
    {
        while (--_0x59c3ce)
        {
            _0x314a53['push'](_0x314a53['shift']());
        }
    };
    _0x1f958f(++_0x280ed8);
}(_0x9beb, 0xb5));

这个函数把_0x280ed8 + 1个元素放到数组末尾, 因此在处理这个数组的时候要注意, 别对应错了.

第三部分 数组字符串处理函数

这一部分完成了对于整个js初始化操作, 这里修改了原生atob对于base64的实现, 因此要注意, 如果自己要写转换代码话, 遇到的时候用下面这个处理之后的, 最开始忘记在这里处理过了, 调试的时候显示的都是对的, 自己写代码验证部分结果的时候, 出现了问题, 没仔细看的后果.

// 这个是数组内容解码的函数, 实际上第二个参数是没有用到的
var _0xb9be = function (_0x3361ea, _0x2b4802)
{
    _0x3361ea = _0x3361ea - 0x0; // 这里第一个参数是通过字符串传进来, 因此这里起到类型转换的作用
    var _0x4d0f08 = _0x9beb[_0x3361ea]; // 这里 _0x4d0f08 保存的是数组下标对应的值, 也就是, 解密第几个字符串
    // 接下来判断有没有进行过初始化操作, 如果没有的话, 先初始化
    if (_0xb9be['initialized'] === undefined)
    {
        (function ()
        {
            var _0x3af96c = Function('return (function () ' + '{}.constructor(\"return this\")()' + ');');
            var _0x50d0d8 = _0x3af96c(); // 这里实际上返回的是 Window 对象
            var _0x5162cf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
            // 下面这个是判断Window有没有atob这个函数, 如果没有的话生成一个进去.
            _0x50d0d8['atob'] || (_0x50d0d8['atob'] = function (_0x3f2f72)
            {
                var _0x7486a1 = String(_0x3f2f72)['replace'](/=+$/, '');
                for (var _0x472f12 = 0x0, _0xdb4be7, _0x2447c3, _0x1f2c04 = 0x0, _0x54dd47 = ''; _0x2447c3 = _0x7486a1['charAt'](_0x1f2c04++); ~_0x2447c3 && (_0xdb4be7 = _0x472f12 % 0x4 ? _0xdb4be7 * 0x40 + _0x2447c3 : _0x2447c3,
                        _0x472f12++ % 0x4) ? _0x54dd47 += String['fromCharCode'](0xff & _0xdb4be7 >> (-0x2 * _0x472f12 & 0x6)) : 0x0)
                {
                    _0x2447c3 = _0x5162cf['indexOf'](_0x2447c3);
                }
                return _0x54dd47;
            });
        }());
        // 字面含义: base64DecodeUnicode
        _0xb9be['base64DecodeUnicode'] = function (_0x30f259)
        {
            var _0x50c8e5 = atob(_0x30f259);
            var _0x29573d = [];
            for (var _0x19cca4 = 0x0, _0xa04edf = _0x50c8e5['length']; _0x19cca4 < _0xa04edf; _0x19cca4++)
            {
                _0x29573d += '%' + ('00' + _0x50c8e5['charCodeAt'](_0x19cca4)['toString'](0x10))['slice'](-0x2);
            }
            return decodeURIComponent(_0x29573d);
        };
        // 到这里完成初始化操作, 置initialized为true, 添加data属性, 这个相当于是一个字典表, 如果已经解密过的东西就存进去, 下次就不用在解密了.
        _0xb9be['data'] = {};
        _0xb9be['initialized'] = !![];
    }
    // 后面这段是先判断之前有没有对传入的参数进行解密过, 如果解密过的话, 那么就不再解密了.
    var _0x43ab45 = _0xb9be['data'][_0x3361ea];
    if (_0x43ab45 === undefined)
    {
        _0x4d0f08 = _0xb9be['base64DecodeUnicode'](_0x4d0f08);
        _0xb9be['data'][_0x3361ea] = _0x4d0f08;
    }
    else
    {
        _0x4d0f08 = _0x43ab45;
    }
    return _0x4d0f08;
};

在这里, 我们可以手动调用一下这个函数, 来看看具体的解密之后每参数是什么.

const paramsCount = _0x9beb.length;

for (let i = 0; i < paramsCount; i++) {
    _0xb9be(i);
}

console.log('finish');

第四部分 核心逻辑

在调试这一部分之前, 我们先测试一下正常的流程, 我们可以发现, 他需要我们传入一个organization

没办法从原网页中找找吧, 发现他是在这里设置的.

首先来看一下函数的整体流程:

在前面提到过, _0xb9be这个函数是解密开始存储的数组的函数, 在这条语句里面_0xb9be('0x0')这个值是split, ['function'] 是js的一个语法, 这相当于函数. 具体这个语法的细节不在这里讲述了.

下面按照函数执行的顺序进行分析.

CASE 4

这里对于操作函数执行了一边重新映射, 而且这个是一个多对一的映射, 有不同的字符串实际上对应的是同一种操作, 对于这个替换, 在下文中有写代码的还原方案, 在这里先留一个悬念, 这段代码其实就是定义了一个字典, 也没有什么需要解释的了.

CASE 1

if (_0x3be3f8['NVz']('‮', _0x522359)) {
    return;
}

对于CASE 1并没有什么实质性的操作, 这个是判断参数_0x522359是不是空字符串, _0x3be3f8['NVz']这个函数查对应表可知, 这是判断!==的操作, 然后通过调试可以发现这里是不会执行return的.

CASE 2

var _0x43741e = arguments;

对于这一部分, 实际上就是一个赋值, 没什么好说的. 不过我们可以看到编译器给出的错误提示

说明这一部分在CASE 0, 和 CASE 3 中会使用, 这里先留个悬念, 等着到之后我再说它干了什么.

CASE 5

var _0x9a3e7e;

在这一部分, 仅仅声明了一个变量, 也没什么好说的, 这里编译器同样有报错, 同样这个也是在CASE 0CASE 3中使用, 后面就分析这到底是干了什么.

CASE 0

for (_0x9a3e7e = _0x211cf2; _0x9a3e7e < _0x4d9911; _0x9a3e7e++) {
    if (_0x3be3f8["yZA"](typeof _0x43741e[_0x9a3e7e], _0x59d11c)) {
        _0x43741e[_0x9a3e7e] = _0x43741e[_0x9a3e7e][_0x55f3f8](_0x2f824c)[_0xaf79b9]()[_0x32258e](_0x2f824c);
    }
}

这里, 对于这个函数干了什么, 采用动态调试的办法看出来的, 通过上面的截图, 我们可以发现这个循环是对于参数的遍历, 把所有字符串执行了string.split("").reverse().join("")这个操作, 实际上是对于字符串的反转.

简单的翻译一下这段代码

// 这里766是参数个数+1
for (i = 0; i < 766; i++) {
    if (typeof arguments[i] === 'string') {
        arguments[i] = arguments[i].split("").reverse().join("");
    }
}

CASE 3

for (_0x9a3e7e = _0x211cf2; _0x9a3e7e < _0x3be3f8['BzV'](_0x4d9911, _0x138e64); _0x9a3e7e++) {
    var _0x1b88d0 = _0x43741e[_0x9a3e7e];
    _0x43741e[_0x9a3e7e] = _0x43741e[_0x3be3f8['YRz'](_0x4d9911 - _0x9a3e7e, _0x11033f)];
    _0x43741e[_0x4d9911 - _0x9a3e7e - _0x11033f] = _0x1b88d0;
}

同样的方式调试这个CASE, 不过这个CASE 就没有上面那个那么直观了, 通过分析我们可以得到这个函数的可读版

for (i = 0; i < 766 / 2; i++) {
    var _0x1b88d0 = arguments[i];
    arguments[i] = arguments[766 - i - 1];
    arguments[766 - i - 1] = _0x1b88d0;
}

很明显, 这个是对参数做了一次反转, 这个比较简单, 大家看一下就明白了了, 因此在写代码替换参数的时候, 要注意这一点, 否则参数就全乱了, 在这里我采用的方案是直接处理完成之后的参数处下断点, 然后查看arguments, 提取出里面的值就好了.

CASE 6

到这里, 基本的参数传递和初始化就都完成了, 最最核心的部分就这最后一个CASE了, 也是最复杂的一个CASE, 整个这一部分实际上是通过一个无参数的自调用函数完成的.

分析前言

在分析这一段之前, 我先介绍一下这个代码一个执行结构, 便于理解后续的分析, 这个operatorMap实际上和前文提到的是一样的结构.

function general_structure() {
    var operatorMap = {
        'op': function _0x1cd33c(params) {
            return params;
        },
    };

    var runLine = "4|1|0|5|3|2"["split"]('|'),
        step = 0x0;

    while (!![]) {
        switch (runLine[step++]) {
            case '0':
                continue;
            case '1':
                continue;
            case '2':
                return operatorMap['op'](0);
                continue;
            case '3':
                continue;
            case '4':
                continue;
            case '5':
                continue;
        }
        break;
    }
}

简单说明一下上面那一段代码, 首先定义一组运算符表, 然后后面的部分运算通过运算符表来实现, 然后函数的执行流程通过stepswitch来实现, 在下面分析的代码中, 大量存在着这样的结构调用的代码.

这里它这段代码, 几乎每个函数的运算都是通过运算表来实现的, 并且这个运算表是嵌套的, 也就是说, 运算表中的运算可能是查上层运算表来实现的, 因此, 如果不处理一下的话, 读起来跳转来回容易乱了, 而且直接读的体验那是相当的蓝瘦, 来一张图表达我直接读的心情.

因此, 我们首先要对混淆的代码处理一下, 如果完全重写这个这个函数的AST的话, 对于编程来说工作量太大了, 因此这里采用手动先处理一下, 然后再用代码处理, 一般来说这里面运算符分为两大类, 第一大类是普通的二元运算比如+,-,*,/之类, 另一大类是函数调用, 来看一个函数调用的例子.

function _0x676ab8(_0x5c6458, _0x419855, _0x42e2e8) {
        return _0x5c6458(_0x419855, _0x42e2e8);
}

这种相当于是第一个传入函数指针, 后面做参数, 因此我们写解析代码的时候也要区分开, 我直接采用esprima来解析js的AST.

class FuncType:
    """
    0: operation
    1: function
    """

    def __init__(self, func_type, **kwargs):
        self.func_type = func_type
        if func_type == 0:
            self.operator = kwargs['operator']

    def parse_to_str(self, *args):
        if self.func_type == 0:
            # 注意这里要加括号, 否则直接写连续调用会有优先级问题
            return f'({args[0]}) {self.operator} ({args[1]})'
        elif self.func_type == 1:
            return f'{args[0]}({", ".join(args[1:])})'

    def __str__(self):
        if self.func_type == 0:
            return self.operator
        return f'function'

def simple_obj_parse_tree_to_key_operator(js_code=''):
    """
    解析类似于这样的字符串
    var _0x3be3f8 = {
        'yZA': function _0x307f34(_0x2ffb3e, _0x58f1c8) {
            return _0x2ffb3e === _0x58f1c8;
        },
        'NVz': function _0x29aa4a(_0x1e341d, _0x3341db) {
            return _0x1e341d !== _0x3341db;
        },
    }
    :param js_code:
    :return: dict
    """
    tree = esprima.parse(js_code)
    tree = tree.to_dict()

    ret = {}
    # 解析到对象下所有的key
    declaration = tree['body'][0]['declarations'][0]
    for _property in declaration['init']['properties']:
        key = _property['key']['value']
        argument = _property['value']['body']['body'][0]['argument']
        if argument.__contains__('operator'):
            ret[key] = FuncType(0, operator=argument['operator'])
        elif argument.__contains__('callee'):
            ret[key] = FuncType(1)
    return ret

class ParseGrammar:

    def __init__(self, _map, prefix='_0x3be3f8'):
        self.map = _map
        self.prefix = prefix
        self.prefix_off = len(prefix)

    def parse_to_real_func(self, func_str='_0x3be3f8["yZA"](typeof _0x43741e[_0x9a3e7e], _0x59d11c)'):
        func_name = func_str[self.prefix_off + 2:self.prefix_off + 2 + 3]
        func_type = self.map[func_name]
        params_reg = re.compile(r'[(](.*)[)]', re.S)
        args = params_reg.findall(func_str)[0].split(', ')
        return func_type.parse_to_str(*args)

    def replace_func(self, content=''):
        # reg = re.compile(f'{self.prefix}\[.*?\)')
        funcs = re.findall(f'{self.prefix}\[.*?\)', content)
        error_list = []
        for func in funcs:
            # 处理嵌套的情况
            if func.count('(') > 1:
                error_list.append(func)
            else:
                target = self.parse_to_real_func(func)
                if not target:
                    continue
                content = content.replace(func, target)

        for func in error_list:
            start = content.find(func)
            print(start, func, content[start])
            off = self.match_bracket(start, content)
            copied_sub_content = content[start:off]
            sub_content = content[start:off]

            if '\n' in sub_content:
                # 不处理多行函数的逻辑
                continue

            # 提取出所有的子函数
            sub_funcs = re.findall(f'{self.prefix}\[.*?\]', sub_content)
            sub_funcs.reverse()

            for sub_func in sub_funcs:
                start = find_all(sub_func, sub_content)[-1]
                off = self.match_bracket(start, sub_content)
                parsing_str = sub_content[start:off]
                if sub_func.count('(') > 1:
                    print('match error', parsing_str)
                else:
                    target = self.parse_to_real_func(parsing_str)
                    if not target:
                        continue
                    sub_content = sub_content.replace(parsing_str, target)
            content = content.replace(copied_sub_content, sub_content)
        return content

    def match_bracket(self, start, content):
        stack = ['(']
        off = start
        off += self.prefix_off + 8
        while stack:
            if content[off] == '(':
                stack.append('(')
            if content[off] == ')':
                stack.pop()
            off += 1
        return off

def replace_func(func_map_path='', input_path='', output_path='', prefix=''):
    """
    替换代码函数
    :param func_map_path: 这里需要函数对应字典复制到一个新的文件里面, 样式见simple_obj_parse_tree_to_key_operator中的注释
    :param input_path: 源文件路径
    :param output_path: 目标文件路径
    :param prefix: 函数前缀 ex: _0xXXXXXX
    :return:
    """
    js_code = read_js_code(func_map_path)
    with open(input_path, 'r', encoding='utf8') as f:
        content = f.read()
    ret = simple_obj_parse_tree_to_key_operator(js_code)
    parse_grammar = ParseGrammar(ret, prefix=prefix)
    c = parse_grammar.replace_func(content)
    with open(output_path, 'w', encoding='utf8') as f:
        f.write(c)

实际上, 这段代码实际上有bug, 只能正确的解析最外层的两个操作符映射表(_0x3be3f8, _0xffb41f), 不过这足够了了, 后面子函数里面的映射可以复制出来单独执行, 然后手动复制回去, 运行代码只替换这两个, 注意顺序, 先替换完第一个再来第二个, 因为第二个用到了第一个的操作符. 其他的看注释和代码吧, 比较简单, 不在这里啰嗦了.

替换完成之后, 如果每次都按照runLine中的顺序来看的话, 看着看着就不知道自己看到哪里了, 因此, 对于这一部分,
简单写个代码替换一下, 然后顺便吧参数也给替换一下, 便于后续的分析. 说句实话, 下面这一段是我分析后面的代码的时候写的, 写完的时候前面分析出来了, 就懒得在去替换了.

def extra_params_to_const(content, out_path=None):
    """
    替换参数为常量, 这一段代码仅用作提取单个函数公共参数, 如果全文替换的话, 对于分析来说作用不是很大, 原因大家猜猜 ^_^.
    :param content: 需要提取的
    :param out_path: 如果参数是字符串并且形同 : [_0x123456] 这一般来说, 直接替换为 .param
    :return: 参数大家自己复制进去吧, 这里懒得在找地方在插入了.
    """
    with open('./params.json', 'r', encoding='utf8') as f:
        c = f.read()
    params = json.loads(c)
    keys = params.keys()
    may_params_list = set(re.findall('_0x[0-9a-fA-F]*', content))
    for i in may_params_list:
        if i in keys:
            param = params[i]
            # 字符串
            if isinstance(param, str):
                param = f'"{param}"'
                if out_path:
                    content = content.replace(f'[{i}]', f'.{params[i]}')
            print(f"""const {i} = {param};""")
    if out_path:
        with open(out_path, 'w', encoding='utf8') as f:
            f.write(content)

def parse_switch_to_normal(code, steps):
    """
    替换 switch 成为顺序执行的函数, 注意复制的时候, 仅仅吧while循环的部分复制进去, 本人比较懒, 不想写搜索找switch, 复制这一部分同样结构生成的AST结构是一样的.
    :param code: 源代码
    :param steps: 步骤
    :return:
    """
    ast_tree = esprima.parse(code)
    cases = ast_tree['body'][0]['body']['body'][0]['body']['body'][0]['cases']
    tree_map = {}
    for case in cases:
        tree_map[case['test']['value']] = case['consequent'][0]

    steps = steps.split('|')

    new_ast = esprima.parse('')

    for step in steps:
        new_ast.body.push(tree_map[step])

    return escodegen.generate(new_ast)

Tips: 这些代码仅作为参考使用, 用于简化分析流程, 不做健壮性处理, 如果替换失败, 建议手动简单处理一下.

DeviceId 函数分析

经过前面简单的处理, 代码变得稍微易读一些了, 我们先来分析deviceId的生成函数.

先来看一下这个函数的大致逻辑.

var _0x51add7 = "4|1|0|5|3|2"["split"]('|'),
    _0x43168e = 0x0;
while (!![])
{
    switch (_0x51add7[_0x43168e++])
    {
        case '0': // 这个字段按照一定的规则生成一个字符串
            var _0xd7d450 = _0x3dface();
            continue;
        case '1': // 这个值存的是 日期 + 时间拼接而成的字符串, ex: 20200209085248
            var _0x445e29 = _0x478891();
            continue;
        case '2': // 字符串拼接, 不在过多解释.
            return ((_0x3cf9ea + _0x4e31fb) + _0x404cc9);
            continue;
        case '3': // 执行加密算法,_0x93d6ad("smsk_web_" + _0x3cf9ea).substr(0, 14))
            var _0x4e31fb = _0x93d6ad(_0x369169 + _0x3cf9ea)[_0x49c7e6](_0x404cc9, _0x40a4b0);
            continue;
        case '4':
            var _0x369169 = _0x3354ab; // smsk_web_ 参数传入的固定值
            continue;
        case '5': // 之前计算的时间 + x + '00'
            var _0x3cf9ea = _0x445e29 + _0x93d6ad(_0xd7d450) + _0x4ae7c4;
            continue;
    }
    break;
}

先用注释简单剧透一下, 接下来我们分析每一个函数, 这里运算符我替换过了, 具体方法前文说过了, 下文同样操作不再赘述.

_0x478891 函数
var _0x30ac2b = "1|3|11|6|4|2|12|8|7|5|9|0|10"['split']('|'),
    _0x1d6f76 = 0x0;
while (!![])
{
    switch (_0x30ac2b[_0x1d6f76++])
    {
        case '0': // 这一步操作是如果second是一位的话, 前面补0
            _0x5a8ed2 = _0x5a8ed2 <= _0x213420 ? _0x276b92 + _0x5a8ed2 : _0x5a8ed2;
            continue;
        case '1':
            var _0x14e7a3 = new Date();
            continue;
        case '2': // Date.getMinutes().toString()
            var _0x58ac13 = _0x14e7a3[_0x229a98]()[_0x5b12b9]();
            continue;
        case '3': // Date.getFullYear().toString()
            var _0xc907b6 = _0x14e7a3[_0x41cdf7]()[_0x5b12b9]();
            continue;
        case '4': // Date.getHours().toString()
            var _0x4c9b6a = _0x14e7a3[_0x2e052d]()[_0x5b12b9]();
            continue;
        case '5': // 如果hour是一位的话, 前面补0, 和case 0作用相似
            _0x4c9b6a = _0x23ff7d["vbK"](_0x4c9b6a, _0x213420) ? _0x23ff7d["ytJ"](_0x276b92, _0x4c9b6a) : _0x4c9b6a;
            continue;
        case '6': // Date.getDate().toString()
            var _0x1cb8dc = _0x14e7a3[_0x52afce]()[_0x5b12b9]();
            continue;
        case '7': // date 如果是一位的话, 补0
            _0x1cb8dc = _0x23ff7d['vbK'](_0x1cb8dc, _0x213420) ? _0x23ff7d["ytJ"](_0x276b92, _0x1cb8dc) : _0x1cb8dc;
            continue;
        case '8': // month如果是一位的话, 补0
            _0x38949a = _0x23ff7d['qRO'](_0x38949a, _0x213420) ? _0x276b92 + _0x38949a : _0x38949a;
            continue;
        case '9': // minutes如果是一位的话, 补0
            _0x58ac13 = _0x58ac13 <= _0x213420 ? _0x276b92 + _0x58ac13 : _0x58ac13;
            continue;
        case '10': // 最后返回结果是 年 + 月 + 日 + 时 + 分 + 秒 拼接的字符串
            return _0xc907b6 + _0x38949a + _0x1cb8dc + _0x4c9b6a + _0x58ac13 + _0x5a8ed2;
            continue;
        case '11': // _0x5d61d2: Date.getMonth() 因为js中月份是从0开始的, 这里+1
            var _0x38949a = (_0x14e7a3[_0x5d61d2]() + _0x53d471)[_0x5b12b9]();
            continue;
        case '12': // getSeconds().toString()
            var _0x5a8ed2 = _0x14e7a3[_0x1e6f61]()[_0x5b12b9]();
            continue;
    }
    break;
}

这里具体函数的作用在注释中解释的比较详细了, 具体我是怎么知道的, 这个直接调试就好了, 在每个continue处下断点, 直接查看就好了, 这个比较简单, 因为这个代码它传进去了700多个参数, 而且做过参数的位置变换, 因此静态分析的话, 函数参数会识别不出来, 因此, 直接动态调试的话, 可以较快的获取参数的值, 下面截图简单举一个例子:

_0x3dface 函数
function _0x3dface() {
    return _0x3604e2[_0x28f143](_0x525166, function (_0x1e3e31) // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx.replace(/[xy]/g, func)
    {
        var _0x525ade = _0x2f8e00["kkW"](_0x2f8e00["XFr"](Math[_0x4b2e75](), _0x4811d5), _0x404cc9), // (Math.random() * 16) | 0
            _0x5c1bfb = _0x1e3e31 == _0x245fe9 ? _0x525ade : _0x2f8e00["Yfl"](_0x2f8e00['GBL'](_0x525ade, _0x29d463), _0x43c55e); // 如果字符是 x 返回刚才计算的值, 否则返回 (_0x525ade & 3) | 8
        return _0x5c1bfb[_0x5b12b9](_0x4811d5); // 转换为16进制字符串返回
    });
}

我们吧这段代码翻译成为可以读的代码, 因为这段用到了随机数, 因此每次调试的结果可能不一样.

function _0x3dface() {
    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (str) {
        const p1 = (Math.random() * 16) | 0;
        const p2 = str === 'x' ? p1 : (p1 & 3) | 8;
        return p2.toString(16)
    });
}
_0x93d6ad 函数

这个函数是整个deviceId中最长的一部分, 看起来是真的头疼, 下面提取出一段能直接执行的代码, 对于具体每一步对字符的具体操作, 在这里就不详细解释了, 修改之后的代码易读性增加了不少, 这里主要是一堆的位运算, 因此不对每一句做详细的说明了, 仅仅对部分代码做了注释.

function _0x40a6af(_0x518ba9) {
    const _0x53d471 = 1;
    const _0x404cc9 = 0;
    const _0x289ef4 = 2;
    const _0x38134c = 32;
    const _0x17b513 = 5;
    const _0x43c55e = 8;
    const _0x6ed072 = 255;

    let _0x41606b;
    const _0x25b964 = [];
    _0x25b964[(_0x518ba9.length >> _0x289ef4) - (_0x53d471)] = undefined; // 这里生成 _0x518ba9.length >> 2 - 1
    for (_0x41606b = _0x404cc9; (_0x41606b) < (_0x25b964.length); _0x41606b += _0x53d471) // 数组长度循环, 给每个数组赋值初值0
    {
        _0x25b964[_0x41606b] = _0x404cc9;
    }
    const _0x4e4e87 = _0x518ba9.length * _0x43c55e; // _0x518ba9.length * 8
    for (_0x41606b = _0x404cc9; (_0x41606b) < (_0x4e4e87); _0x41606b += _0x43c55e) {
        _0x25b964[(_0x41606b) >> (_0x17b513)] |= (_0x518ba9.charCodeAt(_0x41606b / _0x43c55e) & _0x6ed072) << (_0x41606b) % (_0x38134c);
    }
    return _0x25b964;
}

function _0x332b09(_0x20a4f8, _0x44f5f9) {
    const _0x4d6360 = 65535;
    const _0x4811d5 = 16;
    let _0xf0719a = (_0x20a4f8 & _0x4d6360) + (_0x44f5f9 & _0x4d6360);
    let _0x22723d = ((_0x20a4f8) >> (_0x4811d5) + (_0x44f5f9) >> (_0x4811d5)) + ((_0xf0719a) >> (_0x4811d5));
    return ((_0x22723d) << (_0x4811d5)) | ((_0xf0719a) & (_0x4d6360));
}

function _0x42243a(_0x203aa3, _0x1b26ce) {
    const _0x38134c = 32;
    return ((_0x203aa3) << (_0x1b26ce)) | ((_0x203aa3) >>> ((_0x38134c) - (_0x1b26ce)));
}

function _0x56f079(_0x1cc981, _0x4f0dcc, _0x5b3168, _0x1adbb6, _0x2082ce, _0x4bd120) {
    return _0x332b09(_0x42243a(_0x332b09(_0x332b09(_0x4f0dcc, _0x1cc981), _0x332b09(_0x1adbb6, _0x4bd120)), _0x2082ce), _0x5b3168);
}

function _0x1cad02(_0x5212cc, _0x268c90, _0x947b4, _0x497f44, _0x220c3a, _0x1ea6ea, _0xc26a3a) {
    return _0x56f079(((_0x268c90) & (_0x947b4)) | ((~_0x268c90) & (_0x497f44)), _0x5212cc, _0x268c90, _0x220c3a, _0x1ea6ea, _0xc26a3a);
}

function _0x16a6fc(_0xef5fab, _0x515428, _0x306454, _0x2abe9a, _0x16e267, _0x4feff0, _0x3088f3) {
    return _0x56f079(((_0x515428) & (_0x2abe9a)) | ((_0x306454) & (~_0x2abe9a)), _0xef5fab, _0x515428, _0x16e267, _0x4feff0, _0x3088f3);
}

function _0x778ecb(_0x34be7b, _0x2b8a21, _0x372367, _0x2444ea, _0x414fb9, _0x42b4de, _0x57eeca) {
    return _0x56f079(((_0x2b8a21) ^ (_0x372367)) ^ (_0x2444ea), _0x34be7b, _0x2b8a21, _0x414fb9, _0x42b4de, _0x57eeca);
}

function _0x1ebca3(_0x3f6dbb, _0x4905c5, _0x19374f, _0x515268, _0x3ec243, _0x20994b, _0x358f8f) {
    return _0x56f079(_0x19374f ^ (_0x4905c5) | (~_0x515268), _0x3f6dbb, _0x4905c5, _0x3ec243, _0x20994b, _0x358f8f);
}

function _0x4397a5(_0x205f53, _0x2b76b8) {
    // region 参数
    const _0x59df5e = 271733879;
    const _0x283d4e = 1163531501;
    const _0x507333 = 145523070;
    const _0x17ca27 = 1732584194;
    const _0x3cfad1 = 1044525330;
    const _0x49e854 = 1700485571;
    const _0x5dc490 = 995338651;
    const _0xed1dc = 1444681467;
    const _0x11fce3 = 30611744;
    const _0x40935e = 1069501632;
    const _0x3618c3 = 12;
    const _0x2a3a1e = 343485551;
    const _0x1b7581 = 20;
    const _0x43c55e = 8;
    const _0x4811d5 = 16;
    const _0x2c7460 = 1894986606;
    const _0x194725 = 1309151649;
    const _0x133e7e = 1804603682;
    const _0x3bc718 = 1770035416;
    const _0x102466 = 1839030562;
    const _0x5bbf91 = 1272893353;
    const _0x2b4856 = 1473231341;
    const _0x44ac2b = 643717713;
    const _0x475e1a = 1051523;
    const _0x25e301 = 1094730640;
    const _0x167e55 = 11;
    const _0x4f2be3 = 1200080426;
    const _0x4fa669 = 1530992060;
    const _0x1dc29d = 405537848;
    const _0x4f8903 = 155497632;
    const _0x56469a = 681279174;
    const _0x495d41 = 128;
    const _0x293371 = 6;
    const _0x35affe = 13;
    const _0x497019 = 1120210379;
    const _0x116580 = 530742520;
    const _0x417a11 = 4;
    const _0x56ab15 = 1926607734;
    const _0x276f24 = 660478335;
    const _0x2e89c3 = 421815835;
    const _0x1af8c2 = 389564586;
    const _0x279279 = 76029189;
    const _0x3dbf89 = 1416354905;
    const _0x5641bb = 2054922799;
    const _0x1eb872 = 606105819;
    const _0x1ad4c7 = 7;
    const _0x38134c = 32;
    const _0x3132d5 = 64;
    const _0x53d471 = 1;
    const _0x220708 = 1990404162;
    const _0x362230 = 1560198380;
    const _0x540422 = 568446438;
    const _0x106c8c = 701558691;
    const _0x289ef4 = 2;
    const _0x1ebd5e = 640364487;
    const _0x43f38d = 176418897;
    const _0x4a6c5e = 38016083;
    const _0x34b45f = 2022574463;
    const _0x11b695 = 45705983;
    const _0x2732d3 = 722521979;
    const _0x5c3404 = 1873313359;
    const _0x59a486 = 165796510;
    const _0x30b2ed = 42063;
    const _0xf1f23 = 23;
    const _0x521660 = 1502002290;
    const _0x72864 = 271733878;
    const _0x53181c = 17;
    const _0x213420 = 9;
    const _0x1d7d67 = 1958414417;
    const _0x1e1dbf = 10;
    const _0x2e8f3d = 373897302;
    const _0x165940 = 22;
    const _0x38f6c2 = 358537222;
    const _0x2b4c68 = 40341101;
    const _0x490e94 = 35309556;
    const _0x17b513 = 5;
    const _0x470774 = 1732584193;
    const _0x5aead7 = 1019803690;
    const _0x1657b5 = 1236535329;
    const _0x40a4b0 = 14;
    const _0x398659 = 198630844;
    const _0x3bd72f = 51403784;
    const _0x533906 = 1735328473;
    const _0x1a1561 = 15;
    const _0xe3179 = 378558;
    const _0x362afd = 718787259;
    const _0x379334 = 1126891415;
    const _0x29d463 = 3;
    const _0x222578 = 187363961;
    const _0x30069c = 57434055;
    const _0x1c17e7 = 680876936;
    const _0x2cbb52 = 21;
    // endregion

    _0x205f53[_0x2b76b8 >> _0x17b513] |= _0x495d41 << (_0x2b76b8 % _0x38134c);
    _0x205f53[((((_0x2b76b8 + _0x3132d5) >>> _0x213420) << _0x417a11) + _0x40a4b0)] = _0x2b76b8;
    var _0x36321c;
    var _0x5d5468;
    var _0x4694c2;
    var _0x12bd1d;
    var _0x43de48;
    var _0x1b1522 = _0x470774;
    var _0x1737aa = -_0x59df5e;
    var _0x70c0f0 = -_0x17ca27;
    var _0x28fed1 = _0x72864;
    for (_0x36321c = 0; (_0x36321c < _0x205f53.length); _0x36321c += 16) // 循环数组长度次
    {
        _0x5d5468 = _0x1b1522;
        _0x4694c2 = _0x1737aa;
        _0x12bd1d = _0x70c0f0;
        _0x43de48 = _0x28fed1;
        _0x1b1522 = _0x1cad02(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[_0x36321c], _0x1ad4c7, -_0x1c17e7);
        _0x28fed1 = _0x1cad02(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x53d471)], _0x3618c3, -_0x1af8c2);
        _0x70c0f0 = _0x1cad02(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x289ef4)], _0x53181c, _0x1eb872);
        _0x1737aa = _0x1cad02(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x29d463)], _0x165940, -_0x3cfad1);
        _0x1b1522 = _0x1cad02(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x417a11)], _0x1ad4c7, -_0x43f38d);
        _0x28fed1 = _0x1cad02(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x17b513)], _0x3618c3, _0x4f2be3);
        _0x70c0f0 = _0x1cad02(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x293371)], _0x53181c, -_0x2b4856);
        _0x1737aa = _0x1cad02(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x1ad4c7)], _0x165940, -_0x11b695);
        _0x1b1522 = _0x1cad02(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[_0x36321c + _0x43c55e], _0x1ad4c7, _0x3bc718);
        _0x28fed1 = _0x1cad02(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x213420)], _0x3618c3, -_0x1d7d67);
        _0x70c0f0 = _0x1cad02(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x1e1dbf)], _0x53181c, -_0x30b2ed);
        _0x1737aa = _0x1cad02(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x167e55)], _0x165940, -_0x220708);
        _0x1b1522 = _0x1cad02(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x3618c3)], _0x1ad4c7, _0x133e7e);
        _0x28fed1 = _0x1cad02(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x35affe)], _0x3618c3, -_0x2b4c68);
        _0x70c0f0 = _0x1cad02(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x40a4b0)], _0x53181c, -_0x521660);
        _0x1737aa = _0x1cad02(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x1a1561)], _0x165940, _0x1657b5);
        _0x1b1522 = _0x16a6fc(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[_0x36321c + _0x53d471], _0x17b513, -_0x59a486);
        _0x28fed1 = _0x16a6fc(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x293371)], _0x213420, -_0x40935e);
        _0x70c0f0 = _0x16a6fc(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x167e55)], _0x40a4b0, _0x44ac2b);
        _0x1737aa = _0x16a6fc(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[_0x36321c], _0x1b7581, -_0x2e8f3d);
        _0x1b1522 = _0x16a6fc(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x17b513)], _0x17b513, -_0x106c8c);
        _0x28fed1 = _0x16a6fc(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[_0x36321c + _0x1e1dbf], _0x213420, _0x4a6c5e);
        _0x70c0f0 = _0x16a6fc(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x1a1561)], _0x40a4b0, -_0x276f24);
        _0x1737aa = _0x16a6fc(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[_0x36321c + _0x417a11], _0x1b7581, -_0x1dc29d);
        _0x1b1522 = _0x16a6fc(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x213420)], _0x17b513, _0x540422);
        _0x28fed1 = _0x16a6fc(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[_0x36321c + _0x40a4b0], _0x213420, -_0x5aead7);
        _0x70c0f0 = _0x16a6fc(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[_0x36321c + _0x29d463], _0x40a4b0, -_0x222578);
        _0x1737aa = _0x16a6fc(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[_0x36321c + _0x43c55e], _0x1b7581, _0x283d4e);
        _0x1b1522 = _0x16a6fc(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x35affe)], _0x17b513, -_0xed1dc);
        _0x28fed1 = _0x16a6fc(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x289ef4)], _0x213420, -_0x3bd72f);
        _0x70c0f0 = _0x16a6fc(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[_0x36321c + _0x1ad4c7], _0x40a4b0, _0x533906);
        _0x1737aa = _0x16a6fc(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x3618c3)], _0x1b7581, -_0x56ab15);
        _0x1b1522 = _0x778ecb(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x17b513)], _0x417a11, -_0xe3179);
        _0x28fed1 = _0x778ecb(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x43c55e)], _0x167e55, -_0x34b45f);
        _0x70c0f0 = _0x778ecb(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x167e55)], _0x4811d5, _0x102466);
        _0x1737aa = _0x778ecb(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x40a4b0)], _0xf1f23, -_0x490e94);
        _0x1b1522 = _0x778ecb(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x53d471)], _0x417a11, -_0x4fa669);
        _0x28fed1 = _0x778ecb(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x417a11)], _0x167e55, _0x5bbf91);
        _0x70c0f0 = _0x778ecb(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[_0x36321c + _0x1ad4c7], _0x4811d5, -_0x4f8903);
        _0x1737aa = _0x778ecb(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x1e1dbf)], _0xf1f23, -_0x25e301);
        _0x1b1522 = _0x778ecb(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x35affe)], _0x417a11, _0x56469a);
        _0x28fed1 = _0x778ecb(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[_0x36321c], _0x167e55, -_0x38f6c2);
        _0x70c0f0 = _0x778ecb(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x29d463)], _0x4811d5, -_0x2732d3);
        _0x1737aa = _0x778ecb(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[_0x36321c + _0x293371], _0xf1f23, _0x279279);
        _0x1b1522 = _0x778ecb(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[_0x36321c + _0x213420], _0x417a11, -_0x1ebd5e);
        _0x28fed1 = _0x778ecb(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[_0x36321c + _0x3618c3], _0x167e55, -_0x2e89c3);
        _0x70c0f0 = _0x778ecb(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[_0x36321c + _0x1a1561], _0x4811d5, _0x116580);
        _0x1737aa = _0x778ecb(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x289ef4)], _0xf1f23, -_0x5dc490);
        _0x1b1522 = _0x1ebca3(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[_0x36321c], _0x293371, -_0x398659);
        _0x28fed1 = _0x1ebca3(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x1ad4c7)], _0x1e1dbf, _0x379334);
        _0x70c0f0 = _0x1ebca3(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x40a4b0)], _0x1a1561, -_0x3dbf89);
        _0x1737aa = _0x1ebca3(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x17b513)], _0x2cbb52, -_0x30069c);
        _0x1b1522 = _0x1ebca3(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x3618c3)], _0x293371, _0x49e854);
        _0x28fed1 = _0x1ebca3(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x29d463)], _0x1e1dbf, -_0x2c7460);
        _0x70c0f0 = _0x1ebca3(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[_0x36321c + _0x1e1dbf], _0x1a1561, -_0x475e1a);
        _0x1737aa = _0x1ebca3(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x53d471)], _0x2cbb52, -_0x5641bb);
        _0x1b1522 = _0x1ebca3(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x43c55e)], _0x293371, _0x5c3404);
        _0x28fed1 = _0x1ebca3(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x1a1561)], _0x1e1dbf, -_0x11fce3);
        _0x70c0f0 = _0x1ebca3(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x293371)], _0x1a1561, -_0x362230);
        _0x1737aa = _0x1ebca3(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x35affe)], _0x2cbb52, _0x194725);
        _0x1b1522 = _0x1ebca3(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[_0x36321c + _0x417a11], _0x293371, -_0x507333);
        _0x28fed1 = _0x1ebca3(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x167e55)], _0x1e1dbf, -_0x497019);
        _0x70c0f0 = _0x1ebca3(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[_0x36321c + _0x289ef4], _0x1a1561, _0x362afd);
        _0x1737aa = _0x1ebca3(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x213420)], _0x2cbb52, -_0x2a3a1e);
        _0x1b1522 = _0x332b09(_0x1b1522, _0x5d5468);
        _0x1737aa = _0x332b09(_0x1737aa, _0x4694c2);
        _0x70c0f0 = _0x332b09(_0x70c0f0, _0x12bd1d);
        _0x28fed1 = _0x332b09(_0x28fed1, _0x43de48);
    }
    return [_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1];
}

function _0x5c092c(_0x1bd018) {
    const _0x38134c = 32;
    let _0x1dec70 = "";
    const _0x40b0cf = (_0x1bd018.length * _0x38134c);
    for (let i = 0; i < _0x40b0cf; i += 8) {
        _0x1dec70 += String.fromCharCode(((_0x1bd018[i >> 5] >>> (i % _0x38134c)) & 255));
    }
    return _0x1dec70;
}

function _0x15542d(_0x594448) {
    return _0x5c092c(_0x4397a5(_0x40a6af(_0x594448), (_0x594448.length * 8)));
}

function _0x350acb(_0x1a0b22) {
    return unescape(encodeURIComponent(_0x1a0b22));
}

function _0x9a5ac2(_0x2a91c1) {
    return _0x15542d(_0x350acb(_0x2a91c1));
}

function _0x4d8d59(_0x4847c6) {
    const _0x417a11 = 4;
    const _0x404cc9 = 0;
    const _0x1a1561 = 15;

    var _0x2a5904 = "0123456789abcdef";
    var _0x264f43 = "";
    var _0x549ff0;
    var i;
    for (i = _0x404cc9; i < _0x4847c6.length; i += 1) {
        _0x549ff0 = _0x4847c6.charCodeAt(i);
        _0x264f43 += (_0x2a5904.charAt(((_0x549ff0 >>> _0x417a11) & _0x1a1561)) + _0x2a5904.charAt(_0x549ff0 & _0x1a1561));
    }
    return _0x264f43;
}

function _0x1b3f72(_0x2634c5) {
    return _0x4d8d59(_0x9a5ac2(_0x2634c5));
}

/**
 * 字符变换算法, 显然, 这个函数实际上只用到了第一个参数, 其他参数是没用的.
 * @Param _0x173f04
 * @param _0x2d847a
 * @param _0x4b432b
 * @returns {undefined}
 * @private
 */
function _0x93d6ad(_0x173f04, _0x2d847a, _0x4b432b) {
    return _0x1b3f72(_0x173f04);
}

我们来验证一下提取的函数是否正确, 下断点调试一下, 可以发现结果是一致的.

剩余部分

对于剩下的部分就比较简单了, 有两次调用上面的加密算法, 然后就是字符串拼接了, 到这里整个deviceId生成的算法就结束了, 在这里没有看出它的唯一性, 每次运行之后都是不一样的, 在这里稍微剧透一下, 这除了这个deviceId还上传了很多别的参数, 虽然每次执行都会去计算一个deviceID但是实际上传的并不一定是这个哦, 预知后事, 请看下文.

smdata 生成分析

接下来, 后面函数非常多, 我们一一去读代码显然这个工作量太大了, 而且也没太多的必要, 因此, 我们需要找接下来的入手点, 来帮助我们简化分析的流程. 我们可以看到这里这个js发送了一个网络请求.

先来采用惯用的字符串搜索大法, 我们可以发现有两个地方出现了它(smdata), 在出现的地方下断点, 运行我们可以发现实际调用的地方.

这里为什么采用字符串搜索大法, 而不采用下XHR/fetch断点, 在这里断点是没法生效的, 具体原因我也不是很了解, 从测试的情况来说是这样的, 如果有大佬知道欢迎解释一下.

通过简单地分析, 我们可以找到关键的调用函数var _0x12849f = _0x1299b5(), 接下来对这个函数里面的东西进行分析.

function _0x1299b5() {
    var _0x1cac62 = "5|4|3|0|2|9|1|6|8|7"["split"]('|'),
        _0xb43315 = 0x0;
    while (!![]) {
        switch (_0x1cac62[_0xb43315++]) {
            case '0':
                var _0xb84a70 = _0xa7e2e6[_0x13bb62] || _0xc14837;
                continue;
            case '1':
                if (_0x3a2d34) {
                    _0x5cfe05[_0x51d249] = _0x3a2d34;
                }
                continue;
            case '2':
                var _0x5cfe05 = {
                    'channel': _0xb84a70,
                    'deviceId': _0x53f8ad,
                    'plugins': _0x59cb98(),
                    'ua': _0x274fb7(_0x24a6c8),
                    'appVer': _0x274fb7(_0x3a1112),
                    'lang': _0x274fb7(_0x4d03cd),
                    'userLang': _0x274fb7(_0x3de3d6),
                    'browserLang': _0x274fb7(_0x4e905a),
                    'systemLang': _0x274fb7(_0x432e0f),
                    'langs': _0x274fb7(_0x4315cc),
                    'canvas': _0x5cdf41(),
                    'timezone': _0x24c687(),
                    'time': _0x4368e0,
                    'platform': _0x14919c(),
                    'url': _0x4314ed(),
                    'referer': _0x4f01d3(),
                    'res': _0x3e6435(), // 获取分辨率
                    'status': _0x1c3efc(), // 获取状态信息, 比如cookie等
                    'clientSize': _0x528c5b(), // 当前客户端的大小
                    'appCodeName': _0x2876be(),
                    'appName': _0x34e8ee(),
                    'oscpu': _0x3fff2f(),
                    'area': _0x539ed1(),
                    'sid': _0x343076,
                    'version': _0x321175,
                    'subVersion': _0x2d6456
                };
                continue;
            case '3':
                var _0x4368e0 = (_0x3596e2) - (_0x8d7b6d);
                continue;
            case '4':
                var _0x3596e2 = +new Date();
                continue;
            case '5':
                var _0x53f8ad = _0x4714d1() || _0x5d6280()[_0x1d2405];
                continue;
            case '6':
                if (_0x52abcd) {
                    _0x5cfe05[_0x63a450] = _0x52abcd;
                }
                continue;
            case '7':
                return _0x5cfe05;
                continue;
            case '8':
                if (_0xa7e2e6[_0x5a50b0]) {
                    _0x5cfe05[_0x1fd220] = _0x364808();
                }
                continue;
            case '9':
                if (_0x4b713b) {
                    _0x5cfe05[_0x495a89] = _0x4b713b;
                }
                continue;
        }
        break;
    }
}

下面我们按照顺序对部分_0x5cfe05中的具体的值进行分析, 有一些比较简单会一带而过.

channel

这个值是undefined, 调试一下很容易发现, 在这里不贴图了.

deviceId

注意上文提到的彩蛋在这里解释一下, 这个deviceId不是之前生成的, 而是在缓存中的, 下面分析一下读取方式.

可以发现如下代码var _0x53f8ad = _0x4714d1() || _0x5d6280()[_0x1d2405];

先来分析一下取deviceId的函数

_0x4714d1() - 从缓存中读取 deviceId

先来看一下我简化之后的代码, 看到上面的那些参数, 不难想到它把deviceId存放的位置, 在这里, 用到了多处缓存, 然后缓存的key是smidV2.

/**
 * 从缓存中取deviceId
 * @private
 */
function _0x4714d1() {
    const _0x3a0487 = "smidV2";
    const _0x214b01 = "flash";
    const _0x21a328 = "cookie";
    const _0x1b22c5 = "userData";
    const _0xc14837 = "";
    const _0x4aae89 = "session";
    const _0x32ddce = "global";
    const _0x56e997 = "local";

    var _0x4a8f2d = {};
    var _0x5e79bc = _0xc14837;
    if (!_0x5e79bc || !_0x4d528e(_0x5e79bc)) {
        _0x4a8f2d = new _0x15a267(_0x21a328);
        _0x5e79bc = _0x4a8f2d.get(_0x3a0487);
    }
    if (!_0x5e79bc || !_0x4d528e(_0x5e79bc)) {
        _0x4a8f2d = new _0x15a267(_0x56e997);
        _0x5e79bc = _0x4a8f2d.get(_0x6f9fe0);
    }
    if (!_0x5e79bc || !_0x4d528e(_0x5e79bc)) {
        _0x4a8f2d = new _0x15a267(_0x4aae89);
        _0x5e79bc = _0x4a8f2d.get(_0x6f9fe0);
    }
    if (!_0x5e79bc || !_0x4d528e(_0x5e79bc)) {
        _0x4a8f2d = new _0x15a267(_0x214b01);
        _0x5e79bc = _0x4a8f2d.get(_0x6f9fe0);
    }
    if (!_0x5e79bc || !_0x4d528e(_0x5e79bc)) {
        _0x4a8f2d = new _0x15a267(_0x1b22c5);
        _0x5e79bc = _0x4a8f2d.get(_0x6f9fe0);
    }
    if (!_0x5e79bc || !_0x4d528e(_0x5e79bc)) {
        _0x4a8f2d = new _0x15a267(_0x32ddce);
        _0x5e79bc = _0x4a8f2d.get(_0x6f9fe0);
    }
    if (_0x5e79bc && _0x4d528e(_0x5e79bc)) {
        _0x22ce34(_0x5e79bc);
    }
}

这里面有一个简单的校验函数_0x4d528e(), 代码如下, 还是比较容易看明白的:

/**
 * 这个函数的作用是检测deviceId是否合法, 63位[a-z0-9]字符串
 * @param _0x427b65
 * @returns {boolean}
 * @private
 */
function _0x4d528e(_0x427b65) {
    const _0x3b614d = 63;
    var _0x38c20a = /[a-z0-9]{63}/;
    if (_0x427b65.length !== _0x3b614d) {
        return false;
    } else return _0x38c20a.test(_0x427b65);
}

我们分析上面的代码可以发现, 他这里用的 or 操作, 因此如果_0x5e79bc中一旦可以匹配的值的话, 就不会在去取了.

然后我们可以看到最后一个分支语句, _0x22ce34的作用是写入缓存, 代码如下:

/***
 * 这个函数式分别向 global, flash, session, local, cookie, userData 中写入 deviceId
 * @param _0x3946eb
 * @private
 */
function _0x22ce34(_0x3946eb) {
    const _0x6f9fe0 = "smidV2";
    const _0x32ddce = "global";
    const _0x214b01 = "flash";
    const _0x4aae89 = "session";
    const _0x56e997 = "local";
    const _0x1b22c5 = "userData";
    const _0x21a328 = "cookie";

    var _0xd40449 = {};
    _0xd40449 = new _0x15a267(_0x21a328);
    _0xd40449.set(_0x6f9fe0, _0x3946eb);
    _0xd40449 = new _0x15a267(_0x56e997);
    _0xd40449.set(_0x6f9fe0, _0x3946eb);
    _0xd40449 = new _0x15a267(_0x4aae89);
    _0xd40449.set(_0x6f9fe0, _0x3946eb);
    _0xd40449 = new _0x15a267(_0x32ddce);
    _0xd40449.set(_0x6f9fe0, _0x3946eb);
    _0xd40449 = new _0x15a267(_0x214b01);
    _0xd40449.set(_0x6f9fe0, _0x3946eb);
    _0xd40449 = new _0x15a267(_0x1b22c5);
    _0xd40449.set(_0x6f9fe0, _0x3946eb);
}

对于普通的缓存比较容易理解, 接下来我们简单介绍一下userData是怎么操作的, 在这里仅做简单介绍因为这个是针对ie6ie7处理的, 都2020了, 要是开发用ie6/ie7的话, 体验就太酸爽了.

简单处理一下代码, 这里应该是存在了dom里面, 具体我手头暂时没找到那么低版本的IE, 暂时没有测试.

/**
 * 检测是不是ie6/7
 * @returns {boolean}
 * @private
 */
function _0x31b10e() {
    var _0x441d97 = navigator;
    const _0x1ad4c7 = 7;
    const _0x16d091 = /msie ([\d.]+)/;
    const _0x293371 = 6;
    const _0x53d471 = 1;

    var _0xde3a7e = _0x441d97.userAgent.toLowerCase();
    var _0x222ed3 = _0xde3a7e.match(_0x16d091);
    var _0x57c70c = _0x222ed3 && _0x222ed3[_0x53d471];
    return (_0x57c70c) == (_0x293371) || (_0x57c70c) == (_0x1ad4c7);
}

function _0x15a267(_0x30555) {
    const _0x21a328 = "cookie";
    const _0x1b22c5 = "userData";
    const _0x2d4fb0 = "hidden";
    const _0x357d2f = "none";
    const _0x23da07 = null;
    const _0x7459b2 = 365;
    const _0x49ba81 = "#default#userData";
    const _0x4ea12d = "input";
    const _0x1ce23b = "smUserDataStore";
    var _0xe9ec96 = document;

    this.type = (_0x30555) || (_0x21a328);
    if ((_0x30555) == (_0x1b22c5)) {
        var _0x18f065 = _0x31b10e();
        this.storeName = _0x1ce23b;
        if (_0x18f065) {
            var _0x33124e = new Date();
            this.store = _0xe9ec96.createElement(_0x4ea12d);
            this.store.type = _0x2d4fb0;
            this.store.style.display = _0x357d2f;
            this.store.addBehavior(_0x49ba81);
            _0xe9ec96.body.appendChild(this.store);
            _0x33124e.setDate(_0x33124e.getDate() + _0x7459b2);
            this.store.expires = _0x33124e.toUTCString();

        } else {
            this.store = _0x23da07;
        }
    }
}

如果这些都没有, 也就是第一次加载这个的话, 会执行后面的哪一个函数_0x5d6280().

/**
 * 获取新的deviceId
 * @returns {{sign: null, deviceId: string, timestamp: int}}
 * @private
 */
function _0x5d6280() {
    // 这里是之前讲到过的deviceID
    var _0x144f49 = _0x334b81();
    const _0x23da07 = null;

    var _0x476d34 = {
        'key': _0xa36868(_0x28594d),
        'deviceId': _0x144f49,
        'timestamp': _0xa36868(_0x515454)
    };
    var _0xa680e5 = _0x4714d1() || _0x476d34.deviceId;
    var _0x458f5c = _0x476d34.timestamp;
    return {
        'deviceId': _0xa680e5,
        'sign': _0x23da07,
        'timestamp': _0x458f5c
    };
}

这里在用新的deviceId之前, 会再次从缓存中取一边, 在这里我不知道为什么, 调用的是和刚才一样的函数, 可能是为了加大分析的难度吧. ^_^

这样对于deviceId的处理就结束了, 我们接着往下看.

plugins

这里检测了用户浏览器所安装的插件, 用的是navigator.plugins进行获取的, 所有的插件 name + filename + length + description 排序后拼接起来.

/***
 * 检查浏览器所使用的插件, 拼接成字符串后返回.
 * @returns {string}
 * @private
 */
function _0x59cb98() {
    const _0x14e5f4 = /\s/g;
    const _0xc14837 = "";
    const _0x2e5e0a = "Shockwave Flash";
    const _0xea78a5 = "-";
    const _0x404cc9 = 0;
    var _0x441d97 = navigator;

    var _0x5ccc7e = [];
    var _0x50fdc8 = _0xc14837;
    try {
        for (var i = _0x404cc9; i < _0x441d97.plugins.length; i++) {
            var _0x54616d = _0x441d97.plugins[i];
            var _0x5c062f = _0x54616d.description.indexOf(_0x2e5e0a) < _0x404cc9 ? _0x54616d.description : _0xc14837;
            _0x5ccc7e.push(((_0x54616d.name + _0x5c062f) + _0x54616d.filename) + _0x54616d.length);
        }
        _0x5ccc7e.sort();
        _0x50fdc8 = _0x5ccc7e.join();
        _0x50fdc8 = !_0x50fdc8 ? _0xea78a5 : _0x50fdc8.replace(_0x14e5f4, _0xc14837);
    } catch (_0x387e1e) {
        _0x452bc0(_0x387e1e);
    }
    return _0x50fdc8;
}

加下来我们看看异常处理函数干了什么, 修改源码, 在上面随便一个位置添加throw 0;, 抛一个异常出去, 然后异常处下断点, 可以发现他返回的是拼接异常信息一个字符串, 但是在这里他没有接收返回值, 我也没发现全局变量, 因此这里仅仅是普通的异常处理. 这里最后返回的是空字符串, 如果没插件的话返回的是-.

function _0x452bc0(_0x33b4fb) {
    const _0x4c8c40 = "&random=";
    const _0x20fe01 = "|";
    const _0x19b884 = "_";
    const _0x42a368 = "?organization=";
    const _0x45be91 = "no_stack";
    const _0x404cc9 = 0;
    const _0xc14837 = "";
    const _0x27b7df = "&error=";
    const _0x139b05 = ":";
    const _0x1f47a0 = {};

    var _0x8703b4 = _0x33b4fb;
    var _0x2c17a8 = new Image();
    var _0x24e35f = _0xa7e2e6.staticHost;
    var _0x912236 = _0xa7e2e6.organization;
    var _0x2ad49c = _0xa7e2e6.errorPath;
    var _0x41861f = Math.random();
    var _0x446dee = _0xc14837;
    if (_0x33b4fb instanceof Error) {
        var _0x195fb0 = _0x33b4fb.name;
        var _0x8703b4 = _0x33b4fb.message;
        var _0x2947a3 = _0x33b4fb.lineNumber || _0x404cc9;
        var _0x1fc2ca = _0x33b4fb.columnNumber || _0x404cc9;
        var _0x390da0 = _0x33b4fb.stack || _0x45be91;
        var _0x5df798 = _0x195fb0 + _0x139b05 + _0x8703b4 + _0x20fe01 + _0x2947a3 + _0x139b05 + _0x1fc2ca + _0x20fe01 + _0x390da0;
        _0x8703b4 = _0x5df798.replace(_0x1f47a0, _0x19b884);
    }
    _0x446dee = _0x37a5d6 + _0x24e35f + _0x2ad49c + _0x42a368 + _0x912236 + _0x27b7df + _0x8703b4 + _0x4c8c40 + _0x41861f;
    return _0x446dee
}

navigator 相关信息

后面这几个字段(ua, appVer, lang, userLang, browserLang, systemLang, langs, platform, appCodeName, appName, oscpu), 都是直接读取的navigator中的内容, 读者自己看看代码吧, 就是普通对象的取值, 不啰嗦了, 下面直接给出具体取了什么.

const _0x24a6c8 = "userAgent";
const _0x4d03cd = "language";
const _0x4e905a = "browserLanguage";
const _0x432e0f = "systemLanguage";
const _0x3de3d6 = "userLanguage";
const _0x3a1112 = "appVersion";
const _0x4315cc = "languages";

canvas

这里采用了canvas指纹技术, 对于这个技术, 在这里我不过多解释了, 有兴趣的可以参考Hovav Shacham: Pixel Perfect这篇论文. 我们来看看这个函数吧

function _0x471dab(_0x21ff08) {
    const _0x276b92 = "0";
    const _0x404cc9 = 0;
    const _0xc14837 = "";
    const _0x4811d5 = 16;
    const _0x289ef4 = 2;

    var i,
        _0x2bdc88,
        _0x24c559 = _0xc14837,
        _0x508ff1;

    _0x21ff08 += _0xc14837;

    for (i = 0, _0x2bdc88 = _0x21ff08.length; i < _0x2bdc88; i++) {
        _0x508ff1 = _0x21ff08.charCodeAt(i).toString(_0x4811d5);
        _0x24c559 += (_0x508ff1.length) < (_0x289ef4) ? (_0x276b92) + (_0x508ff1) : _0x508ff1;
    }
    return _0x24c559;
}

function _0x5cdf41() {
    const _0x417a11 = 4;
    const _0x53181c = 17;
    const _0x4811d5 = 16;
    const _0x289ef4 = 2;
    const _0x5db728 = "data:image/png;base64,";
    const _0x103904 = 60;
    const _0x3618c3 = 12;
    const _0x29b114 = "top";
    const _0x4b5d26 = "rgba(120, 180, 0, 0.7)";
    const _0x181eee = "http://www.ishumei.com";
    const _0x5cc13f = "#e88";
    const _0x1a1561 = 15;
    const _0xb00a2e = "24px 'Arial'";
    const _0x49d420 = "2d";
    const _0x165940 = 22;
    const _0x4c8505 = "canvas";
    const _0x35ac50 = 120;
    const _0x53d471 = 1;
    const _0xc14837 = "";
    const _0x23d3ac = "alphabetic";
    const _0x35b862 = "#f99";

    try {
        var _0x1ac6c3 = document.createElement(_0x4c8505);
        var _0x2ffc74 = _0x1ac6c3.getContext(_0x49d420);
        var _0x113369 = _0x181eee;
        _0x2ffc74.textBaseline = _0x29b114;
        _0x2ffc74.font = _0xb00a2e;
        _0x2ffc74.textBaseline = _0x23d3ac;
        _0x2ffc74.fillStyle = _0x5cc13f;
        _0x2ffc74.fillRect(_0x35ac50, _0x53d471, _0x103904, _0x165940);
        _0x2ffc74.fillStyle = _0x35b862;
        _0x2ffc74.fillText(_0x113369, _0x289ef4, _0x1a1561);
        _0x2ffc74.fillStyle = _0x4b5d26;
        _0x2ffc74.fillText(_0x113369, _0x417a11, _0x53181c);
        var _0xf1f04c = _0x1ac6c3.toDataURL().replace(_0x5db728, _0xc14837);
        var _0x3a5c90 = atob(_0xf1f04c);
        var _0x1de6ef = _0x471dab(_0x3a5c90.slice(-_0x4811d5, -_0x3618c3));
        return _0x1de6ef;
    } catch (_0x1ecc29) {
        _0x452bc0(_0x1ecc29);
        return _0xc14837;
    }
}

这些都是基本的绘图操作, 这里最后会返回一个16进制字符串. 在支持canvas的浏览器都会生成对应指纹, 悄悄的说一句, 之前的IE6不是白检测的, 哈哈.

timezone

这里调用的函数new Date().getTimezoneOffset()

time

这个时间是从进入6执行开始, 到读取time这个之前所用的时间, 下图为开始计时的时刻, 我猜测这里是检测调试使用的, 一旦下断点, 这个时间必然贼长.

url

取自: location.href.substr(0, 100)

referer

取自: document.referer.substr(0, 100)

res

这里获取的是色彩深度和屏幕宽高等信息.

function _0x3e6435() {
    const _0x19b884 = "_";

    var _0x189982 = screen;
    var _0xc9717a = _0x189982.width;
    var _0x3cb1e5 = _0x189982.height;
    var _0x343b86 = _0x189982.colorDepth;
    return _0xc9717a + _0x19b884 + _0x3cb1e5 + _0x19b884 + _0x343b86;
}

status

这里主要检测了cookie, flash, selenium, debug状态, 在这里检测了主流爬虫工具的特征.

/**
 * 检测是否有Flash
 * @returns {number}
 * @private
 */
function _0x335336() {
    const _0xe9ec96 = document;
    const _0x543ad9 = "ShockwaveFlash.ShockwaveFlash";
    const _0x404cc9 = 0;
    const _0x53d471 = 1;

    let _0x1bfff7 = _0x404cc9;

    try {
        if (_0xe9ec96.all) {
            var _0x160ec4 = new ActiveXObject(_0x543ad9);
            if (_0x160ec4) {
                _0x1bfff7 = _0x53d471;
            }
        } else {
            if (_0x441d97.plugins && (_0x441d97.plugins.length > _0x404cc9)) {
                var _0x160ec4 = _0x441d97.plugins["Shockwave Flash"];
                if (_0x160ec4) {
                    _0x1bfff7 = _0x53d471;
                }
            }
        }
    } catch (_0x2e4c78) {
        _0x1bfff7 = _0x404cc9;
        _0x452bc0(_0x2e4c78);
    }
    return _0x1bfff7;
}

function _0x319de3() {
    var _0x59f07b = "00000000";
    const _0xc14837 = "";
    const _0x1ad4c7 = 7;
    const _0x53d471 = 1;
    var _0x4a8167 = 0;

    var _0x588106 = _0x59f07b.split(_0xc14837);
    try {
        var _0x5ef8a7 = _0x5c282e();
        if (_0x5ef8a7) {
            _0x4a8167 = _0x53d471;
            _0x588106[_0x1ad4c7] = _0x53d471;
        }
    } catch (_0x7fc582) {
    }
    _0x59f07b = _0x588106.join(_0xc14837);
    return _0x4a8167;
}

/**
 * 这里会检测 phantom, nightmare, selenium, 等爬虫工具
 * @returns {boolean}
 * @private
 */
function _0x5c282e() {
    const _0x207ab1 = "__webdriver_evaluate";
    const _0x146400 = "webdriver";
    const _0x57be01 = "Sequentum";
    const _0x152dcb = "__selenium_evaluate";
    const _0x2b2767 = "__webdriver_script_func";
    const _0x4da0c9 = "_Selenium_IDE_Recorder";
    const _0x261dbe = {};
    const _0x53d471 = 1;
    const _0x3b7fb8 = "_phantom";
    const _0x5341d3 = "__nightmare";
    const _0x40bc04 = "__fxdriver_evaluate";
    const _0x1101b1 = "callSelenium";
    const _0x27a4fa = "__driver_unwrapped";
    const _0x1a7633 = "__webdriver_script_function";
    const _0x553eb4 = "_selenium";
    const _0x3ada7c = "callPhantom";
    const _0x2c770b = true;
    const _0x1b15f6 = "__webdriver_script_fn";
    const _0x6fa32f = "driver";
    const _0x442448 = "__selenium_unwrapped";
    const _0xca0390 = false;
    const _0x500970 = "__driver_evaluate";
    const _0x4d3b38 = "selenium";
    const _0x1c2bea = "__fxdriver_unwrapped";
    const _0x2f4c6d = "__webdriver_unwrapped";

    try {
        var _0x3e47d8 = [
            _0x207ab1,
            _0x152dcb,
            _0x1a7633,
            _0x2b2767,
            _0x1b15f6,
            _0x40bc04,
            _0x27a4fa,
            _0x2f4c6d,
            _0x500970,
            _0x442448,
            _0x1c2bea
        ];
        var _0x171dd6 = [
            _0x3b7fb8,
            _0x5341d3,
            _0x553eb4,
            _0x3ada7c,
            _0x1101b1,
            _0x4da0c9
        ];
        for (var _0x391ccc in _0x171dd6) {
            var _0x4db8b5 = _0x171dd6[_0x391ccc];
            if (window[_0x4db8b5]) {
                return _0x2c770b;
            }
        }
        for (var _0x2b989e in _0x3e47d8) {
            var _0x59228c = _0x3e47d8[_0x2b989e];
            if (window.document[_0x59228c]) {
                return _0x2c770b;
            }
        }
        for (var _0x312588 in window.document) {
            if (_0x312588.match(_0x261dbe) && window.document[_0x312588].cache_) {
                return _0x2c770b;
            }
        }
        if (window.external && window.external.toString() && window.external.toString().indexOf(_0x57be01) != -_0x53d471)
            return _0x2c770b;
        if (window.document.documentElement.getAttribute(_0x4d3b38))
            return _0x2c770b;
        if (window.document.documentElement.getAttribute(_0x146400))
            return _0x2c770b;
        if (window.document.documentElement.getAttribute(_0x6fa32f))
            return _0x2c770b;
        if (window.navigator.webdriver)
            return _0x2c770b;
        return _0xca0390;
    } catch (_0x26513c) {
        return _0xca0390;
    }
}

/***
 * 检测调试
 * @returns {number}
 * @private
 */
function _0x324f4b() {
    const _0xa7e2e6 = {
        "organization": "****",
        "staticHost": "static.fengkongcloud.com",
        "apiHost": "fp-it.fengkongcloud.com",
        "errorPath": "/dist/web/v2.0.0/null.png",
        "flashUrl": "/dist/web/v2.0.0/fp.swf",
        "apiPath": "/v3/profile/web",
        "isOpenUserBehavior": true,
        "monitorGroupSeparator": ";",
        "monitorValSeparator": ",",
        "pointsMax": 35
    };
    const _0x200c02 = "id";
    const _0xca0390 = false;
    const _0x53d471 = 1;
    const _0xf542a5 = "__BROWSERTOOLS_DOMEXPLORER_ADDED";
    const _0x404cc9 = 0;

    const _0x1f15cf = window;

    const _0x37a5d6 = document.location.protocol === 'https' ? 'https://' : 'http://';

    const _0x18e970 = {
        "isFirstConsole": true,
        "isInput": false,
        "mouseClickCount": 0,
        "keyPressCount": 0,
        "mousemove": {
            "x": [],
            "y": [],
            "t": []
        },
        "mousedown": {
            "x": [],
            "y": [],
            "t": []
        },
        "scroll": {
            "y": [],
            "t": []
        },
        "keyup": []
    };

    try {
        var _0x156544 = _0xa7e2e6.staticHost;
        var _0x580390 = _0xa7e2e6.errorPath;
        var _0x4908ae = _0x404cc9;
        if (!!_0x1f15cf['__IE_DEVTOOLBAR_CONSOLE_COMMAND_LINE'] || _0xf542a5 in _0x1f15cf) {
            _0x4908ae = _0x53d471;
            return _0x4908ae;
        }
        var _0x2e82f1 = new Image();
        _0x2e82f1.src = _0x37a5d6 + _0x156544 + _0x580390;
        _0x2e82f1.__defineGetter__(_0x200c02, function () {
            _0x4908ae = _0x53d471;
        });
        if (_0x1f15cf.console && _0x18e970.isFirstConsole) {
            console.log(_0x2e82f1);
            _0x18e970.isFirstConsole = _0xca0390;
        }
        if (_0x1f15cf.Firebug && _0x1f15cf.Firebug.chrome && _0x1f15cf.Firebug.chrome.isInitialized) {
            _0x4908ae = _0x53d471;
            return _0x4908ae;
        }
        return _0x4908ae;
    } catch (_0x815661) {
        return _0x404cc9;
    }
}

/**
 * 这里分别检测三种特征, 以及cookie是否可用, 具体三种特征见上面对应函数的注释
 * @returns {string}
 * @private
 */
function _0x1c3efc() {
    const _0xc14837 = "";
    const _0x53d471 = 1;
    const _0x404cc9 = 0;
    const _0x428ca9 = "sm_test_";
    const _0x21a328 = "cookie";
    const _0x1bad3c = "sm_test_cookie_enable";

    var _0x584c8a = _0xc14837;
    var _0x4a2869 = _0x335336();
    var _0x4425a0 = _0x319de3();
    var _0x19610a = _0x324f4b();

    var _0xe9ec96 = document;

    _0x584c8a += ((_0x4a2869) + (_0xc14837)) + (_0x4425a0);
    if (!_0xe9ec96.cookie && !_0x441d97.cookieEnabled) {
        _0x584c8a += _0x404cc9;
    } else {
        var _0x360d09 = _0x1bad3c;
        var _0x142575 = _0x428ca9 + Math.random();
        var _0x50066f = new _0x15a267(_0x21a328);
        _0x50066f.set(_0x360d09, _0x142575);
        var _0x4c4026 = _0x50066f.get(_0x360d09);
        _0x50066f.remove(_0x360d09);
        if (_0x142575 == _0x4c4026) {
            _0x584c8a += _0x53d471;
        } else {
            _0x584c8a += _0x404cc9;
        }
    }
    _0x584c8a += _0x19610a;
    return _0x584c8a;
}

clientSize

这个函数检测了当前浏览器的显示大小等信息, 代码显示的比较清楚

function _0x528c5b() {
    const _0x404cc9 = 0;
    const _0x19b884 = "_";

    var _0x1f15cf = window;
    var _0xe9ec96 = document;

    var _0x213fb0 = _0x1f15cf.mozInnerScreenX || _0x1f15cf.screenLeft || _0x404cc9;
    var _0x32b639 = _0x1f15cf.mozInnerScreenY || _0x1f15cf.screenTop || _0x404cc9;
    var _0x7a9f3d = _0xe9ec96.body;
    var _0x37d5bd = _0x7a9f3d ? _0x7a9f3d.clientWidth : _0x404cc9;
    var _0x3ee7f9 = _0x7a9f3d ? _0x7a9f3d.clientHeight : _0x404cc9;
    var _0x152dcc = screen.width;
    var _0x4d78c9 = screen.height;
    var _0x29fec8 = screen.availWidth;
    var _0x50a96c = screen.availHeight;
    var _0x3ed7c7 = [
        _0x213fb0,
        _0x32b639,
        _0x37d5bd,
        _0x3ee7f9,
        _0x152dcc,
        _0x4d78c9,
        _0x29fec8,
        _0x50a96c
    ];
    return _0x3ed7c7.join(_0x19b884);
}

area

这里好像一直返回"-1_-1", 我调试的时候是这样, 不知道我是否还有没踩到的坑.

sid

这里sid调用的_0x4d939e()这个函数, 这个函数也比较简单, 大家直接看代码吧.

/**
 * 时间戳 + 8位随机整数
 * @returns {*}
 * @private
 */
function _0x4d939e() {
    const _0x22312c = 100000000;
    const _0xea78a5 = "-";

    var _0x2b71d1 = +new Date();
    var _0x1f5001 = Math.floor(Math.random() * _0x22312c);
    return _0x2b71d1 + _0xea78a5 + _0x1f5001;
}

version & subVersion

这里是sdk的版本信息, 在这里我分析的值是2.0.02.0.3, 读者可以自行下断点查看.

sdl

这个获取了某些标签的个数, 具体上传了什么, 看下面的检测代码吧, 调用位置

// sdl check & add
if (_0x52abcd) {
    _0x5cfe05[_0x63a450] = _0x52abcd;
}

具体检测代码.

/**
 * 获取dom中 img, iframe, script, textarea, input, form 标签的个数
 * @returns {string} "-" 分隔
 */
function getSpecialDomLength() {
    const _0xad702a = "img";
    const _0x533983 = "iframe";
    const _0x2aed3f = "script";
    const _0x19b884 = "_";
    const _0x5c2957 = "textarea";
    const _0x4ea12d = "input";
    const _0xc14837 = "";
    const _0xe9ec96 = document;

    try {
        var _0x2bdfde = _0xe9ec96.getElementsByTagName(_0x533983).length;
        var _0x3a2473 = _0xe9ec96.forms.length;
        var _0x998ebe = _0xe9ec96.getElementsByTagName(_0x4ea12d).length;
        var _0x41e829 = _0xe9ec96.getElementsByTagName(_0x5c2957).length;
        var _0x338b1c = _0xe9ec96.getElementsByTagName(_0x2aed3f).length;
        var _0x1caa89 = _0xe9ec96.getElementsByTagName(_0xad702a).length;
        var _0x19113f = [
            _0x2bdfde,
            _0x3a2473,
            _0x998ebe,
            _0x41e829,
            _0x338b1c,
            _0x1caa89
        ];
        return _0x19113f.join(_0x19b884);
    } catch (_0x1d30c9) {
        _0x452bc0(_0x1d30c9);
        return _0xc14837;
    }
}

bebavior

看这个名字, 应该是用户相关行为, 我们直接通过代码来看看, 不过通过代码来看, 这里好像是只添加了mousemove的数据, 其他的我没看到有地方添加进去, 不过在这个地方, 我没想到好的调试方案, 怎样在调试的时候同时能触发这些事件, 如果有大神有好的方案, 欢迎告诉我.

function _0x364808() {
    const _0xd0265c = "mousemove";
    const _0x53d471 = 1;
    const _0x404cc9 = 0;
    const _0x1c4bac = "mousemove=";
    const _0x489f83 = "&";
    const _0xc14837 = "";

    const _0xa7e2e6 = {
        "organization": "****", // 这里实际有值, 介于安全原因, 我不放上来了
        "staticHost": "static.fengkongcloud.com",
        "apiHost": "fp-it.fengkongcloud.com",
        "errorPath": "/dist/web/v2.0.0/null.png",
        "flashUrl": "/dist/web/v2.0.0/fp.swf",
        "apiPath": "/v3/profile/web",
        "isOpenUserBehavior": true,
        "monitorGroupSeparator": ";",
        "monitorValSeparator": ",",
        "pointsMax": 35
    };

    const _0x4a58f3 = {
        'isFirstConsole': _0x2c770b,
        'isInput': _0xca0390,
        'mouseClickCount': _0x404cc9,
        'keyPressCount': _0x404cc9,
        'mousemove':
            {
                'x': [],
                'y': [],
                't': []
            },
        'mousedown':
            {
                'x': [],
                'y': [],
                't': []
            },
        'scroll':
            {
                'y': [],
                't': []
            },
        'keyup': []
    };
    const _0x392ce0 = _0xa7e2e6.monitorGroupSeparator;
    const _0x1c482c = _0xa7e2e6.monitorValSeparator;
    var _0x2cfa4c = [];
    var _0x2f8224;
    for (var _0x4f4f5d in _0x4a58f3) {
        switch (_0x4f4f5d) {
            case _0xd0265c: // mousemove
                var _0x2b39fc = _0x4a58f3[_0x4f4f5d].t.length || _0x404cc9;
                _0x2f8224 = _0xc14837;
                for (var _0x868d2c = _0x404cc9; (_0x868d2c) < (_0x2b39fc); _0x868d2c++) {
                    var _0x4073e7 = Math.floor(_0x4a58f3[_0x4f4f5d].x[_0x868d2c]);
                    var _0x5a64c5 = Math.floor(_0x4a58f3[_0x4f4f5d].y[_0x868d2c]);
                    var _0x23b638 = _0x4a58f3[_0x4f4f5d].t[_0x868d2c];
                    _0x2f8224 += _0x4073e7 + _0x1c482c + _0x5a64c5 + _0x1c482c + _0x23b638;
                    if (_0x868d2c != _0x2b39fc - _0x53d471) {
                        _0x2f8224 += _0x392ce0;
                    }
                }
                _0x2cfa4c.push((_0x1c4bac) + (_0x2f8224));
                break;
        }
    }
    return _0x2cfa4c.join(_0x489f83);
}

监听器是通过这个函数添加进去的, 直接用的addEvent相关函数,

/**
 * 添加mousemove, mousedown 等事件的监听, if 判断是用来兼容浏览器的.
 * @param _0x16bc9b
 * @param _0x4d67ae
 * @param _0x56777b
 * @private
 */
function _0xbe6940(_0x16bc9b, _0x4d67ae, _0x56777b) {
    const _0xca0390 = false;
    const _0x3bcd20 = "on";

    if (_0x16bc9b.addEventListener) {
        _0x16bc9b.addEventListener(_0x4d67ae, _0x56777b, _0xca0390);
    } else if (_0x16bc9b.attachEvent) {
        _0x4d67ae = (_0x3bcd20) + (_0x4d67ae);
        _0x16bc9b.attachEvent(_0x4d67ae, _0x56777b);
    } else {
        _0x4d67ae = (_0x3bcd20) + (_0x4d67ae);
        _0x16bc9b[_0x4d67ae] = _0x56777b;
    }
}

smdata - 剩余部分

在调试过程中, 我还发现了其他的几个字段lip, rip, 虽然源码中有赋值的函数, 但是我在调试中下断点并没有执行到那里, 然后处理一下, 处理成为url编码的格式, 最后返回字符串, 这样用户浏览器的基本特征就获取结束了.

function _0x1a9e8e(_0x5ab315) {
    const _0x489f83 = "&";
    const _0x51f9f9 = "=";

    let _0x41c18d = [];
    for (let _0x2d717f in _0x5ab315) {
        if (_0x5ab315.hasOwnProperty(_0x2d717f)) {
            _0x41c18d.push(_0x2d717f + _0x51f9f9 + encodeURIComponent(_0x5ab315[_0x2d717f]));
        }
    }
    return _0x41c18d.join(_0x489f83);
}

最后数据经过DES加密后base64转码, 然后拼接上时间戳就是最终的smdata, 下面简单分析一下流程, 这里有一个坑, 这里代码实际上是在异常中调用的, 这种代码的调用, 又学到一招, 可以在异常中处理逻辑.

function _0x45fedc(_0x39b5e6, _0x8d74a9) {
    console.log(_0x39b5e6, _0x8d74a9);
    try {
        var _0x1619f4 = _0x167c2c();
        var _0x54ea9f = _0x1619f4.timestamp;
        var _0x446a44 = _0x39b5e6(_0x8d74a9);
        return _0x4f1e31(_0x446a44) + _0x54ea9f;
    } catch (_0x1c87ec) {
        var _0x1619f4 = _0x5d6280();
        var _0x54ea9f = _0x1619f4.timestamp;
        // des 加密
        var _0x446a44 = _0x2e6f29(_0x56da84, _0x8d74a9);
        // 转码base64 + timestamp
        return (_0x4f1e31(_0x446a44)) + (_0x54ea9f);
    }
}

function _0x46c533(_0x47e91d, _0x52fde7) {
    const _0x53d471 = 1;
    const _0x23da07 = null;
    const _0x404cc9 = 0;
    const _0x29d463 = 3;
    const _0xc14837 = "";
    const _0x3259c8 = ",";
    const _0x1a6e6e = "function";
    const _0x289ef4 = 2;

    var _0x159b77 = "W";
    var _0x17d073 = _0x167c2c();
    var _0x4fc824 = _0x17d073.length;

    try {
        var _0x41a055 = _0x2e6f29(_0x56da84, _0x457c0b(_0x47e91d), _0xc14837, _0x404cc9);
        var _0x46da85 = _0x41a055.substr(_0x404cc9, _0x4fc824).split(_0x3259c8);
        var _0x119341 = _0x5d410f(_0x46da85[_0x404cc9]);
        var _0x15393b = _0x23da07;
        if (typeof _0x119341 == _0x1a6e6e) {
            _0x15393b = function (_0x2bb1d9) {
                return _0x2e6f29(_0x119341, _0x2bb1d9, _0x46da85[_0x289ef4], _0x46da85[_0x53d471], _0x46da85[_0x29d463]);
            };
        }
        return (_0x159b77) + _0x45fedc(_0x15393b);
    } catch (_0x2f767e) {
        // 执行前面会报错, 在异常中完成的一部分代码, 这里实际上最终是 'W' + base64(des(smdata)) + timestamp
        return _0x159b77 + _0x45fedc(_0x56da84, _0x52fde7);
    }
}

对于加密算法的密钥, 我是直接在des加密算法执行中下断点看出来的, 在这里, 为了安全起见, 我不在这明文贴出密钥了.

小结

到这里对于smdata中的数据来源就都分析完成了, 在这里它还是检测了蛮多信息的, 包括常用的爬虫工具, canvas指纹, 鼠标移动的信息, 浏览器插件信息等等, 我猜测这应该是根据这些信息来判断生成的deviceID是否是可用.

数据上传 & 回调

/**
 * jsonp 发送请求
 * @param _0x53ca0f
 * @param _0x734e18
 * @param _0x307885
 * @private
 */
function _0x3556e8(_0x53ca0f, _0x734e18, _0x307885) {
    _0x53ca0f = "http://fp-it.fengkongcloud.com/v3/profile/web";

    _0x734e18 = {
        "organization": "",
        "smdata": "",
        "os": "web",
        "version": "2.0.0"
    };

    const _0x437170 = "&_=";
    const _0xfbe897 = "smCB_";
    const _0x4e3761 = "head";
    const _0x472b12 = "loaded";
    const _0x349c6c = "complete";
    const _0x2aed3f = "script";
    const _0x778616 = "text/javascript";
    const _0x20f39a = 100;
    const _0xc14837 = "";
    const _0x299746 = 999;
    const _0x404cc9 = 0;
    const _0x23da07 = null;
    const _0x2acb1c = 10000;
    const _0xca0390 = false;
    const _0x15666a = "?callback=";
    const _0x3e6e19 = "time out";
    const _0x489f83 = "&";
    const _0x2c770b = true;
    var _0xe9ec96 = document;
    var _0x1f15cf = window;

    var _0x6a08fc = new Date().getTime() + _0xc14837;

    // 创建一个script dom
    var _0x16fadc = _0xe9ec96.createElement(_0x2aed3f);
    // 拼接成为 url get 参数
    var _0x417783 = _0x1a9e8e(_0x734e18);
    // smCB_ + timestamp
    var _0x54adb4 = _0xfbe897 + _0x6a08fc;
    var _0x30581a = "smJsonpScript";
    var _0x299d3f = _0xca0390;
    var _0x3fb62d = _0x404cc9;

    // 从这里设置执行的回调
    _0x1f15cf[_0x54adb4] = function (_0x3eaf11) {
        var _0x46d168 = _0xe9ec96.getElementById(_0x30581a);
        clearTimeout(_0x3fb62d);
        if (_0x307885) {
            _0x307885(_0x3eaf11);
            try {
                _0x46d168.parentNode.removeChild(_0x46d168);
                delete _0x1f15cf[_0x54adb4];
                _0x1f15cf[_0x54adb4] = _0x23da07;
            } catch (_0x5dc01f) {
                _0x452bc0(_0x5dc01f);
            }
        }
    };

    _0x3fb62d = setTimeout(function () {
        // 这个函数是 生成key deviceId 对象
        var _0xc7cbaa = _0x5d6280();
        if (_0x1f15cf[_0x54adb4]) {
            _0x1f15cf[_0x54adb4]({
                'code': _0x299746,
                'message': _0x3e6e19,
                'deviceId': _0xc7cbaa.deviceId,
                'detail': {
                    'timestamp': _0xc7cbaa.timestamp,
                    'sign': _0xc7cbaa.sign
                }
            });
        }
    }, _0x2acb1c);

    _0x16fadc.type = _0x778616;
    _0x16fadc.id = _0x30581a;
    _0x16fadc.onload = _0x16fadc.onreadystatechange = function () {
        if (!_0x299d3f && (!this.readyState || (this.readyState) === (_0x472b12) || this.readyState === _0x349c6c)) {
            _0x299d3f = _0x2c770b;
            _0x16fadc.onload = _0x16fadc.onreadystatechange = _0x23da07;
        }
    };
    _0x16fadc.src = _0x53ca0f + _0x15666a + _0x54adb4 + _0x489f83 + _0x417783 + _0x437170 + _0x6a08fc;

    // 这里动态插入了一个JS脚本
    (function () {
        var _0x27baf1 = _0xe9ec96.getElementsByTagName(_0x4e3761)[_0x404cc9] || _0xe9ec96.documentElement;
        if (!_0x27baf1) {
            setTimeout(arguments.callee, _0x20f39a);
            return;
        }
        _0x27baf1.insertBefore(_0x16fadc, _0x27baf1.firstChild);
    }());
}

这个是用的jsonp, 代码看一下注释应该不难理解, 对于jsonp的相关知识, 在这里我就不展开了, 有兴趣的自己查阅相关资料吧 ^.^

总结

分析完这个js文件, 读代码的过程确实痛苦, 但是最终还是大体分析出了这个文件干了什么, 由于个人水平有限难免会出现错误, 也欢迎大家指正. 不过通过对于这个文件的分析, 还是学到了很多技巧的.

  • 设置超长的参数, 并在函数内部利用argument调整参数顺序
  • 对于操作符, 单独提取成为字典, 多个字典进行嵌套
  • 通过异常调用函数, 增加代码静态分析的难度
  • 添加对于函数执行时间的检测, 用于判断是否有外力干扰函数的执行
  • 从多方位检测用户的合理性, 把关键数据写入多个缓存的地方
  • 修改对于部分原生函数的实现, 增大提取代码的难度

声明

本教程仅可作为研究学习为目的, 请勿用作其他用途. 读者将其信息用作其他用途, 由读者自行承担全部法律及连带责任, 与本文作者无关, 本文版权归作者所有, 转载请注明来源.

原始样本.txt

185.15 KB, 下载次数: 52, 下载积分: 吾爱币 -1 CB

免费评分

参与人数 17吾爱币 +19 热心值 +14 收起 理由
JASONSHOW + 1 老哥,确实厉害。
df_2015 + 1 用心讨论,共获提升!
VikyPluto + 1 + 1 谢谢@Thanks!
xieyi1393 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
eb9lu + 1 + 1 谢谢@Thanks!
pass101 + 1 + 1 用心讨论,共获提升!
不被承认的好人 + 1 + 1 用心讨论,共获提升!
arryboom + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
__star__ + 1 + 1 谢谢@Thanks!
生有涯知无涯 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
jafck + 1 + 1 用心讨论,共获提升!
KYO_2代 + 1 我很赞同!
wmsuper + 3 + 1 谢谢@Thanks!
lies2014 + 1 + 1 谢谢@Thanks!
涛之雨 + 3 + 1 这一切都要从一只蝙蝠说起。。。
nws0507 + 1 看分析我都看的眼花,大佬牛逼
风景暗色调 + 1 牛逼!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
Meshcher 发表于 2020-4-3 17:13
有什么方法可以让浏览器内window对象下面的所有不可配置属性变为可配置属性,比如设置:window.document=1,console.log(window.document)//1
或者说把所有属性的描述符&#160;configurable设为true
怎么跨域访问dom及注入JS代码,&#160;比如我自己有个www.a.com页面,内部嵌入iframe&#160;src="www.abc.com",我怎么获取到iframe内部的dom及所有window下的所有属性,以及在加载iframe前注入自己的js代码,比如内部页面设置把window.document的值显示到内部页面的一个DIV里,&#160;我在外部页面注入一段JS{window.document=1(你没看错,就是要设置document的值,让他生效,如果设置其他的能改的值就没必要发布这个任务了)},内部页面的div里显示的就是1,然后在外部页面获取这个DIV的值然后改变.注意内部页面是跨域,所有内部页面的东西我们做不了任何改动,只有外部能获取内部的东西及提前注入JS才能做一些操作,后端抓取,JSONP,等常规的方法就不要来了,这个内部页面比较特殊,必须要在前端加载.
沙发
风景暗色调 发表于 2020-2-13 22:38
3#
lhr349 发表于 2020-2-13 22:59
膜拜楼主!我要是哟哟楼主一半厉害就好了,膜拜楼主!我要是哟哟楼主一半厉害就好了
4#
涛之雨 发表于 2020-2-13 23:39
本帖最后由 涛之雨 于 2020-2-13 23:40 编辑

这一切都要从一只蝙蝠说起。。。。。


膜拜楼主
5#
lies2014 发表于 2020-2-14 00:07
好长好详细,先收藏慢慢看
6#
永恒帝 发表于 2020-2-14 00:07
我的手指滑个不停
7#
千哥哥 发表于 2020-2-14 00:18
膜拜大佬
8#
游水的猪 发表于 2020-2-14 08:56
需要很强的耐心啊~~
9#
冰镇苏打水 发表于 2020-2-14 09:59
莫非也是想斗鱼抽奖才进行分析源码?
10#
 楼主| AnonymousQsh 发表于 2020-2-14 10:03 |楼主
冰镇苏打水 发表于 2020-2-14 09:59
莫非也是想斗鱼抽奖才进行分析源码?

这倒不是, 主要是最近出不去, 在家闲得无聊
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-26 10:20

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表