数某风控JS算法分析
本帖最后由 AnonymousQsh 于 2020-2-13 22:22 编辑> 近来闲来无事, 然后打算分析一下数某风控中的js算法, 来提升一下自己的能力.
**样本来源**: [某鱼登录页面](http://passport.douyu.com/index/login)的(http://static.fengkongcloud.com/fpv2.js) 这个直接获取可能会变, 我将我当时分析的样本上传到附件.
拿到代码, 我们可以发现这个代码是被混淆过的, 因此我们需要先简单处理一下, 便于我们阅读, 这里我直接找的一个js代码反混淆的[网站(prettifyjs)](https://www.prettifyjs.net/), 这里会对hex字符串进行转码, 阅读起来会容易一点.
代码可以分为4部分, 接下来将对每一部分分别讲解作用, 悄悄的说一句, 重点都在第四部分上
# 第一部分: 变量名称存储数组
这里存储了一些在函数中用到的变量和字符串, 这个数组函数挺多的, 在这里我就不都贴上来了.
```js
var _0x9beb = ['cG9UbmVlcmNz', 'eWRvYg==', ..., 'WW5lZXJjU3Jlbm5Jem9t'];
```
# 第二部分 数组处理函数
```js
/**
* 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的实现, 因此要注意, 如果自己要写转换代码话, 遇到的时候用下面这个处理之后的, 最开始忘记在这里处理过了, 调试的时候显示的都是对的, 自己写代码验证部分结果的时候, 出现了问题, 没仔细看的后果.
```js
// 这个是数组内容解码的函数, 实际上第二个参数是没有用到的
var _0xb9be = function (_0x3361ea, _0x2b4802)
{
_0x3361ea = _0x3361ea - 0x0; // 这里第一个参数是通过字符串传进来, 因此这里起到类型转换的作用
var _0x4d0f08 = _0x9beb; // 这里 _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'];
if (_0x43ab45 === undefined)
{
_0x4d0f08 = _0xb9be['base64DecodeUnicode'](_0x4d0f08);
_0xb9be['data'] = _0x4d0f08;
}
else
{
_0x4d0f08 = _0x43ab45;
}
return _0x4d0f08;
};
```
在这里, 我们可以手动调用一下这个函数, 来看看具体的解密之后每参数是什么.
```js
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
```js
if (_0x3be3f8['NVz']('', _0x522359)) {
return;
}
```
对于`CASE 1`并没有什么实质性的操作, 这个是判断参数`_0x522359`是不是空字符串, `_0x3be3f8['NVz']`这个函数查对应表可知, 这是判断`!==`的操作, 然后通过调试可以发现这里是不会执行return的.
## CASE 2
```js
var _0x43741e = arguments;
```
对于这一部分, 实际上就是一个赋值, 没什么好说的. 不过我们可以看到编译器给出的错误提示
说明这一部分在`CASE 0`, 和 `CASE 3` 中会使用, 这里先留个悬念, 等着到之后我再说它干了什么.
## CASE 5
```js
var _0x9a3e7e;
```
在这一部分, 仅仅声明了一个变量, 也没什么好说的, 这里编译器同样有报错, 同样这个也是在`CASE 0`和`CASE 3`中使用, 后面就分析这到底是干了什么.
## CASE 0
```js
for (_0x9a3e7e = _0x211cf2; _0x9a3e7e < _0x4d9911; _0x9a3e7e++) {
if (_0x3be3f8["yZA"](typeof _0x43741e, _0x59d11c)) {
_0x43741e = _0x43741e(_0x2f824c)()(_0x2f824c);
}
}
```
这里, 对于这个函数干了什么, 采用动态调试的办法看出来的, 通过上面的截图, 我们可以发现这个循环是对于参数的遍历, 把所有字符串执行了`string.split("").reverse().join("")`这个操作, 实际上是对于字符串的反转.
简单的翻译一下这段代码
```js
// 这里766是参数个数+1
for (i = 0; i < 766; i++) {
if (typeof arguments === 'string') {
arguments = arguments.split("").reverse().join("");
}
}
```
## CASE 3
```js
for (_0x9a3e7e = _0x211cf2; _0x9a3e7e < _0x3be3f8['BzV'](_0x4d9911, _0x138e64); _0x9a3e7e++) {
var _0x1b88d0 = _0x43741e;
_0x43741e = _0x43741e(_0x4d9911 - _0x9a3e7e, _0x11033f)];
_0x43741e = _0x1b88d0;
}
```
同样的方式调试这个CASE, 不过这个CASE 就没有上面那个那么直观了, 通过分析我们可以得到这个函数的可读版
```js
for (i = 0; i < 766 / 2; i++) {
var _0x1b88d0 = arguments;
arguments = arguments;
arguments = _0x1b88d0;
}
```
很明显, 这个是对参数做了一次反转, 这个比较简单, 大家看一下就明白了了, 因此在写代码替换参数的时候, 要注意这一点, 否则参数就全乱了, 在这里我采用的方案是直接处理完成之后的参数处下断点, 然后查看`arguments`, 提取出里面的值就好了.
## CASE 6
到这里, 基本的参数传递和初始化就都完成了, 最最核心的部分就这最后一个`CASE`了, 也是最复杂的一个`CASE`, 整个这一部分实际上是通过一个无参数的自调用函数完成的.
### 分析前言
在分析这一段之前, 我先介绍一下这个代码一个执行结构, 便于理解后续的分析, 这个`operatorMap`实际上和前文提到的是一样的结构.
```js
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) {
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的话, 对于编程来说工作量太大了, 因此这里采用手动先处理一下, 然后再用代码处理, 一般来说这里面运算符分为两大类, 第一大类是普通的二元运算比如`+,-,*,/`之类, 另一大类是函数调用, 来看一个函数调用的例子.
```js
function _0x676ab8(_0x5c6458, _0x419855, _0x42e2e8) {
return _0x5c6458(_0x419855, _0x42e2e8);
}
```
这种相当于是第一个传入函数指针, 后面做参数, 因此我们写解析代码的时候也要区分开, 我直接采用`esprima`来解析js的AST.
```python
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}) {self.operator} ({args})'
elif self.func_type == 1:
return f'{args}({", ".join(args)})'
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']['declarations']
for _property in declaration['init']['properties']:
key = _property['key']['value']
argument = _property['value']['body']['body']['argument']
if argument.__contains__('operator'):
ret = FuncType(0, operator=argument['operator'])
elif argument.__contains__('callee'):
ret = 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, _0x59d11c)'):
func_name = func_str
func_type = self.map
params_reg = re.compile(r'[(](.*)[)]', re.S)
args = params_reg.findall(func_str).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)
off = self.match_bracket(start, content)
copied_sub_content = content
sub_content = content
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
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 == '(':
stack.append('(')
if content == ')':
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`中的顺序来看的话, 看着看着就不知道自己看到哪里了, 因此, 对于这一部分,
简单写个代码替换一下, 然后顺便吧参数也给替换一下, 便于后续的分析. 说句实话, 下面这一段是我分析后面的代码的时候写的, 写完的时候前面分析出来了, 就懒得在去替换了.
```python
def extra_params_to_const(content, out_path=None):
"""
替换参数为常量, 这一段代码仅用作提取单个函数公共参数, 如果全文替换的话, 对于分析来说作用不是很大, 原因大家猜猜 ^_^.
:param content: 需要提取的
:param out_path: 如果参数是字符串并且形同 : 这一般来说, 直接替换为 .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*', content))
for i in may_params_list:
if i in keys:
param = params
# 字符串
if isinstance(param, str):
param = f'"{param}"'
if out_path:
content = content.replace(f'[{i}]', f'.{params}')
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']['body']['body']['body']['body']['cases']
tree_map = {}
for case in cases:
tree_map['value']] = case['consequent']
steps = steps.split('|')
new_ast = esprima.parse('')
for step in steps:
new_ast.body.push(tree_map)
return escodegen.generate(new_ast)
```
> Tips: 这些代码仅作为参考使用, 用于简化分析流程, 不做健壮性处理, 如果替换失败, 建议手动简单处理一下.
### DeviceId 函数分析
经过前面简单的处理, 代码变得稍微易读一些了, 我们先来分析`deviceId`的生成函数.
先来看一下这个函数的大致逻辑.
```js
var _0x51add7 = "4|1|0|5|3|2"["split"]('|'),
_0x43168e = 0x0;
while (!![])
{
switch (_0x51add7)
{
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)(_0x404cc9, _0x40a4b0);
continue;
case '4':
var _0x369169 = _0x3354ab; // smsk_web_ 参数传入的固定值
continue;
case '5': // 之前计算的时间 + x + '00'
var _0x3cf9ea = _0x445e29 + _0x93d6ad(_0xd7d450) + _0x4ae7c4;
continue;
}
break;
}
```
先用注释简单剧透一下, 接下来我们分析每一个函数, 这里运算符我替换过了, 具体方法前文说过了, 下文同样操作不再赘述.
#### `_0x478891` 函数
```js
var _0x30ac2b = "1|3|11|6|4|2|12|8|7|5|9|0|10"['split']('|'),
_0x1d6f76 = 0x0;
while (!![])
{
switch (_0x30ac2b)
{
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()();
continue;
case '3': // Date.getFullYear().toString()
var _0xc907b6 = _0x14e7a3()();
continue;
case '4': // Date.getHours().toString()
var _0x4c9b6a = _0x14e7a3()();
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()();
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() + _0x53d471)();
continue;
case '12': // getSeconds().toString()
var _0x5a8ed2 = _0x14e7a3()();
continue;
}
break;
}
```
这里具体函数的作用在注释中解释的比较详细了, 具体我是怎么知道的, 这个直接调试就好了, 在每个`continue`处下断点, 直接查看就好了, 这个比较简单, 因为这个代码它传进去了700多个参数, 而且做过参数的位置变换, 因此静态分析的话, 函数参数会识别不出来, 因此, 直接动态调试的话, 可以较快的获取参数的值, 下面截图简单举一个例子:
#### `_0x3dface` 函数
```js
function _0x3dface() {
return _0x3604e2(_0x525166, function (_0x1e3e31) // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx.replace(//g, func)
{
var _0x525ade = _0x2f8e00["kkW"](_0x2f8e00["XFr"](Math(), _0x4811d5), _0x404cc9), // (Math.random() * 16) | 0
_0x5c1bfb = _0x1e3e31 == _0x245fe9 ? _0x525ade : _0x2f8e00["Yfl"](_0x2f8e00['GBL'](_0x525ade, _0x29d463), _0x43c55e); // 如果字符是 x 返回刚才计算的值, 否则返回 (_0x525ade & 3) | 8
return _0x5c1bfb(_0x4811d5); // 转换为16进制字符串返回
});
}
```
我们吧这段代码翻译成为可以读的代码, 因为这段用到了随机数, 因此每次调试的结果可能不一样.
```js
function _0x3dface() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(//g, function (str) {
const p1 = (Math.random() * 16) | 0;
const p2 = str === 'x' ? p1 : (p1 & 3) | 8;
return p2.toString(16)
});
}
```
#### `_0x93d6ad` 函数
这个函数是整个`deviceId`中最长的一部分, 看起来是真的头疼, 下面提取出一段能直接执行的代码, 对于具体每一步对字符的具体操作, 在这里就不详细解释了, 修改之后的代码易读性增加了不少, 这里主要是一堆的位运算, 因此不对每一句做详细的说明了, 仅仅对部分代码做了注释.
```js
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 = _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 |= _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, _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, _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, _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, _0x1b7581, -_0x2e8f3d);
_0x1b1522 = _0x16a6fc(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x17b513)], _0x17b513, -_0x106c8c);
_0x28fed1 = _0x16a6fc(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53, _0x213420, _0x4a6c5e);
_0x70c0f0 = _0x16a6fc(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x1a1561)], _0x40a4b0, -_0x276f24);
_0x1737aa = _0x16a6fc(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53, _0x1b7581, -_0x1dc29d);
_0x1b1522 = _0x16a6fc(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53[(_0x36321c) + (_0x213420)], _0x17b513, _0x540422);
_0x28fed1 = _0x16a6fc(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53, _0x213420, -_0x5aead7);
_0x70c0f0 = _0x16a6fc(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53, _0x40a4b0, -_0x222578);
_0x1737aa = _0x16a6fc(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53, _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, _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, _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, _0x167e55, -_0x38f6c2);
_0x70c0f0 = _0x778ecb(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53[(_0x36321c) + (_0x29d463)], _0x4811d5, -_0x2732d3);
_0x1737aa = _0x778ecb(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53, _0xf1f23, _0x279279);
_0x1b1522 = _0x778ecb(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53, _0x417a11, -_0x1ebd5e);
_0x28fed1 = _0x778ecb(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53, _0x167e55, -_0x2e89c3);
_0x70c0f0 = _0x778ecb(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53, _0x4811d5, _0x116580);
_0x1737aa = _0x778ecb(_0x1737aa, _0x70c0f0, _0x28fed1, _0x1b1522, _0x205f53[(_0x36321c) + (_0x289ef4)], _0xf1f23, -_0x5dc490);
_0x1b1522 = _0x1ebca3(_0x1b1522, _0x1737aa, _0x70c0f0, _0x28fed1, _0x205f53, _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, _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, _0x293371, -_0x507333);
_0x28fed1 = _0x1ebca3(_0x28fed1, _0x1b1522, _0x1737aa, _0x70c0f0, _0x205f53[(_0x36321c) + (_0x167e55)], _0x1e1dbf, -_0x497019);
_0x70c0f0 = _0x1ebca3(_0x70c0f0, _0x28fed1, _0x1b1522, _0x1737aa, _0x205f53, _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 ;
}
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 % _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()`, 接下来对这个函数里面的东西进行分析.
```js
function _0x1299b5() {
var _0x1cac62 = "5|4|3|0|2|9|1|6|8|7"["split"]('|'),
_0xb43315 = 0x0;
while (!![]) {
switch (_0x1cac62) {
case '0':
var _0xb84a70 = _0xa7e2e6 || _0xc14837;
continue;
case '1':
if (_0x3a2d34) {
_0x5cfe05 = _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();
continue;
case '6':
if (_0x52abcd) {
_0x5cfe05 = _0x52abcd;
}
continue;
case '7':
return _0x5cfe05;
continue;
case '8':
if (_0xa7e2e6) {
_0x5cfe05 = _0x364808();
}
continue;
case '9':
if (_0x4b713b) {
_0x5cfe05 = _0x4b713b;
}
continue;
}
break;
}
}
```
下面我们按照顺序对部分`_0x5cfe05`中的具体的值进行分析, 有一些比较简单会一带而过.
### `channel`
这个值是`undefined`, 调试一下很容易发现, 在这里不贴图了.
### `deviceId`
注意上文提到的彩蛋在这里解释一下, 这个deviceId不是之前生成的, 而是在缓存中的, 下面分析一下读取方式.
可以发现如下代码`var _0x53f8ad = _0x4714d1() || _0x5d6280();`
先来分析一下取`deviceId`的函数
#### `_0x4714d1()` - 从缓存中读取 `deviceId`
先来看一下我简化之后的代码, 看到上面的那些参数, 不难想到它把`deviceId`存放的位置, 在这里, 用到了多处缓存, 然后缓存的key是`smidV2`.
```js
/**
* 从缓存中取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()`, 代码如下, 还是比较容易看明白的:
```js
/**
* 这个函数的作用是检测deviceId是否合法, 63位字符串
* @param _0x427b65
* @returns {boolean}
* @private
*/
function _0x4d528e(_0x427b65) {
const _0x3b614d = 63;
var _0x38c20a = /{63}/;
if (_0x427b65.length !== _0x3b614d) {
return false;
} else return _0x38c20a.test(_0x427b65);
}
```
我们分析上面的代码可以发现, 他这里用的 `or` 操作, 因此如果`_0x5e79bc`中一旦可以匹配的值的话, 就不会在去取了.
然后我们可以看到最后一个分支语句, `_0x22ce34`的作用是写入缓存, 代码如下:
```js
/***
* 这个函数式分别向 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, 暂时没有测试.
```js
/**
* 检测是不是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;
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()`.
```js
/**
* 获取新的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` 排序后拼接起来.
```js
/***
* 检查浏览器所使用的插件, 拼接成字符串后返回.
* @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;
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;`, 抛一个异常出去, 然后异常处下断点, 可以发现他返回的是拼接异常信息一个字符串, 但是在这里他没有接收返回值, 我也没发现全局变量, 因此这里仅仅是普通的异常处理. 这里最后返回的是空字符串, 如果没插件的话返回的是`-`.
```js
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`中的内容, 读者自己看看代码吧, 就是普通对象的取值, 不啰嗦了, 下面直接给出具体取了什么.
```js
const _0x24a6c8 = "userAgent";
const _0x4d03cd = "language";
const _0x4e905a = "browserLanguage";
const _0x432e0f = "systemLanguage";
const _0x3de3d6 = "userLanguage";
const _0x3a1112 = "appVersion";
const _0x4315cc = "languages";
```
### `canvas`
这里采用了`canvas指纹技术`, 对于这个技术, 在这里我不过多解释了, 有兴趣的可以参考(https://hovav.net/ucsd/papers/ms12.html)这篇论文. 我们来看看这个函数吧
```js
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`
这里获取的是色彩深度和屏幕宽高等信息.
```js
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`状态, 在这里检测了主流爬虫工具的特征.
```js
/**
* 检测是否有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 = _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;
if (window) {
return _0x2c770b;
}
}
for (var _0x2b989e in _0x3e47d8) {
var _0x59228c = _0x3e47d8;
if (window.document) {
return _0x2c770b;
}
}
for (var _0x312588 in window.document) {
if (_0x312588.match(_0x261dbe) && window.document.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`
这个函数检测了当前浏览器的显示大小等信息, 代码显示的比较清楚
```js
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()`这个函数, 这个函数也比较简单, 大家直接看代码吧.
```js
/**
* 时间戳 + 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`
这个获取了某些标签的个数, 具体上传了什么, 看下面的检测代码吧, 调用位置
```js
// sdl check & add
if (_0x52abcd) {
_0x5cfe05 = _0x52abcd;
}
```
具体检测代码.
```js
/**
* 获取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`的数据, 其他的我没看到有地方添加进去, 不过在这个地方, 我没想到好的调试方案, 怎样在调试的时候同时能触发这些事件, 如果有大神有好的方案, 欢迎告诉我.
```js
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.t.length || _0x404cc9;
_0x2f8224 = _0xc14837;
for (var _0x868d2c = _0x404cc9; (_0x868d2c) < (_0x2b39fc); _0x868d2c++) {
var _0x4073e7 = Math.floor(_0x4a58f3.x);
var _0x5a64c5 = Math.floor(_0x4a58f3.y);
var _0x23b638 = _0x4a58f3.t;
_0x2f8224 += _0x4073e7 + _0x1c482c + _0x5a64c5 + _0x1c482c + _0x23b638;
if (_0x868d2c != _0x2b39fc - _0x53d471) {
_0x2f8224 += _0x392ce0;
}
}
_0x2cfa4c.push((_0x1c4bac) + (_0x2f8224));
break;
}
}
return _0x2cfa4c.join(_0x489f83);
}
```
监听器是通过这个函数添加进去的, 直接用的`addEvent`相关函数,
```js
/**
* 添加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 = _0x56777b;
}
}
```
### `smdata` - 剩余部分
在调试过程中, 我还发现了其他的几个字段`lip`, `rip`, 虽然源码中有赋值的函数, 但是我在调试中下断点并没有执行到那里, 然后处理一下, 处理成为url编码的格式, 最后返回字符串, 这样用户浏览器的基本特征就获取结束了.
```js
function _0x1a9e8e(_0x5ab315) {
const _0x489f83 = "&";
const _0x51f9f9 = "=";
let _0x41c18d = [];
for (let _0x2d717f in _0x5ab315) {
if (_0x5ab315.hasOwnProperty(_0x2d717f)) {
_0x41c18d.push(_0x2d717f + _0x51f9f9 + encodeURIComponent(_0x5ab315));
}
}
return _0x41c18d.join(_0x489f83);
}
```
最后数据经过DES加密后base64转码, 然后拼接上时间戳就是最终的`smdata`, 下面简单分析一下流程, 这里有一个坑, 这里代码实际上是在异常中调用的, 这种代码的调用, 又学到一招, 可以在异常中处理逻辑.
```js
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);
var _0x15393b = _0x23da07;
if (typeof _0x119341 == _0x1a6e6e) {
_0x15393b = function (_0x2bb1d9) {
return _0x2e6f29(_0x119341, _0x2bb1d9, _0x46da85, _0x46da85, _0x46da85);
};
}
return (_0x159b77) + _0x45fedc(_0x15393b);
} catch (_0x2f767e) {
// 执行前面会报错, 在异常中完成的一部分代码, 这里实际上最终是 'W' + base64(des(smdata)) + timestamp
return _0x159b77 + _0x45fedc(_0x56da84, _0x52fde7);
}
}
```
对于加密算法的密钥, 我是直接在des加密算法执行中下断点看出来的, 在这里, 为了安全起见, 我不在这明文贴出密钥了.
### 小结
到这里对于`smdata`中的数据来源就都分析完成了, 在这里它还是检测了蛮多信息的, 包括常用的爬虫工具, canvas指纹, 鼠标移动的信息, 浏览器插件信息等等, 我猜测这应该是根据这些信息来判断生成的`deviceID`是否是可用.
## 数据上传 & 回调
```js
/**
* 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 = function (_0x3eaf11) {
var _0x46d168 = _0xe9ec96.getElementById(_0x30581a);
clearTimeout(_0x3fb62d);
if (_0x307885) {
_0x307885(_0x3eaf11);
try {
_0x46d168.parentNode.removeChild(_0x46d168);
delete _0x1f15cf;
_0x1f15cf = _0x23da07;
} catch (_0x5dc01f) {
_0x452bc0(_0x5dc01f);
}
}
};
_0x3fb62d = setTimeout(function () {
// 这个函数是 生成key deviceId 对象
var _0xc7cbaa = _0x5d6280();
if (_0x1f15cf) {
_0x1f15cf({
'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) || _0xe9ec96.documentElement;
if (!_0x27baf1) {
setTimeout(arguments.callee, _0x20f39a);
return;
}
_0x27baf1.insertBefore(_0x16fadc, _0x27baf1.firstChild);
}());
}
```
这个是用的`jsonp`, 代码看一下注释应该不难理解, 对于`jsonp`的相关知识, 在这里我就不展开了, 有兴趣的自己查阅相关资料吧 ^.^
# 总结
分析完这个`js`文件, 读代码的过程确实痛苦, 但是最终还是大体分析出了这个文件干了什么, 由于个人水平有限难免会出现错误, 也欢迎大家指正. 不过通过对于这个文件的分析, 还是学到了很多技巧的.
- 设置超长的参数, 并在函数内部利用`argument`调整参数顺序
- 对于操作符, 单独提取成为字典, 多个字典进行嵌套
- 通过异常调用函数, 增加代码静态分析的难度
- 添加对于函数执行时间的检测, 用于判断是否有外力干扰函数的执行
- 从多方位检测用户的合理性, 把关键数据写入多个缓存的地方
- 修改对于部分原生函数的实现, 增大提取代码的难度
# 声明
> 本教程仅可作为研究学习为目的, 请勿用作其他用途. 读者将其信息用作其他用途, 由读者自行承担全部法律及连带责任, 与本文作者无关, 本文版权归作者所有, 转载请注明来源. 有什么方法可以让浏览器内window对象下面的所有不可配置属性变为可配置属性,比如设置:window.document=1,console.log(window.document)//1
或者说把所有属性的描述符 configurable设为true
怎么跨域访问dom及注入JS代码, 比如我自己有个www.a.com页面,内部嵌入iframe src="www.abc.com",我怎么获取到iframe内部的dom及所有window下的所有属性,以及在加载iframe前注入自己的js代码,比如内部页面设置把window.document的值显示到内部页面的一个DIV里, 我在外部页面注入一段JS{window.document=1(你没看错,就是要设置document的值,让他生效,如果设置其他的能改的值就没必要发布这个任务了)},内部页面的div里显示的就是1,然后在外部页面获取这个DIV的值然后改变.注意内部页面是跨域,所有内部页面的东西我们做不了任何改动,只有外部能获取内部的东西及提前注入JS才能做一些操作,后端抓取,JSONP,等常规的方法就不要来了,这个内部页面比较特殊,必须要在前端加载.
膜拜楼主!我要是哟哟楼主一半厉害就好了 膜拜楼主!我要是哟哟楼主一半厉害就好了,膜拜楼主!我要是哟哟楼主一半厉害就好了 本帖最后由 涛之雨 于 2020-2-13 23:40 编辑
这一切都要从一只蝙蝠说起。。。。。
膜拜楼主 好长好详细,先收藏慢慢看 我的手指滑个不停 膜拜大佬 需要很强的耐心啊~~{:1_908:} 莫非也是想斗鱼抽奖才进行分析源码? 冰镇苏打水 发表于 2020-2-14 09:59
莫非也是想斗鱼抽奖才进行分析源码?
这倒不是, 主要是最近出不去, 在家闲得无聊:Dweeqw