近来闲来无事, 然后打算分析一下数某风控中的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 0
和CASE 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;
}
}
简单说明一下上面那一段代码, 首先定义一组运算符表, 然后后面的部分运算通过运算符表来实现, 然后函数的执行流程通过step
和switch
来实现, 在下面分析的代码中, 大量存在着这样的结构调用的代码.
这里它这段代码, 几乎每个函数的运算都是通过运算表来实现的, 并且这个运算表是嵌套的, 也就是说, 运算表中的运算可能是查上层运算表来实现的, 因此, 如果不处理一下的话, 读起来跳转来回容易乱了, 而且直接读的体验那是相当的蓝瘦, 来一张图表达我直接读的心情.
因此, 我们首先要对混淆的代码处理一下, 如果完全重写这个这个函数的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
是怎么操作的, 在这里仅做简单介绍因为这个是针对ie6
和ie7
处理的, 都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.0
和2.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
调整参数顺序
- 对于操作符, 单独提取成为字典, 多个字典进行嵌套
- 通过异常调用函数, 增加代码静态分析的难度
- 添加对于函数执行时间的检测, 用于判断是否有外力干扰函数的执行
- 从多方位检测用户的合理性, 把关键数据写入多个缓存的地方
- 修改对于部分原生函数的实现, 增大提取代码的难度
声明
本教程仅可作为研究学习为目的, 请勿用作其他用途. 读者将其信息用作其他用途, 由读者自行承担全部法律及连带责任, 与本文作者无关, 本文版权归作者所有, 转载请注明来源.