文章目录
- [1. 写在前面](#1. 写在前面)
- [2. 接口分析](#2. 接口分析)
- [3. 加密分析](#3. 加密分析)
- [4. 风控分析](#4. 风控分析)
【🏠作者主页】:吴秋霖
【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与开发工作!
【🌟作者推荐】:对爬虫领域以及JS逆向分析感兴趣的朋友可以关注《爬虫JS逆向实战》《深耕爬虫领域》
未来作者会持续更新所用到、学到、看到的技术知识!包括但不限于:各类验证码突防、爬虫APP与JS逆向分析、RPA自动化、分布式爬虫、Python领域等相关文章
作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者无关!如有侵权,请联系作者本人进行删除!
先赞后看、已成习惯~
1. 写在前面
先之前分析了一下Web
端的加密参数跟它的设备指纹风控策略。接下来在再分析一下分析的其他端(APP
跟M
)本期我们先继续来看看它在M
端的接口加密防护以及风控强度
一般很多厂商它们外采第三方的风控防护产品的话一般不管是什么端大部分都会选择用同一家的!但是它这个明显是没有用一家的,Web
用的是某东的然后M
端的话用的是顶像
另外它的一个整体防护强度M
端是做的比Web
要好的很多(至少它的那个行为验证码好像是无法进行绕过的
)
分析网站:
bS5haXJjaGluYS5jb20uY24=
2. 接口分析
它这个端一样不需要登录,直接去查询的接口搜索看看发包情况。请求头里面没有其他明显疑似加密的参数,Cookie
的话看起来是有一些风控相关的参数(毕竟走的是非登录的游客浏览模式
),然后请求参数有一个后缀
参数加密,与Web
端不相同的是这个参数不是请求参数的加密(是各种风控的信息加一些固定参数生成的
)如下所示:

在第一次请求这个数据接口的时候,是需要过行为验证码
的,接口返回信息如下所示:
javascript
{"msg":"成功","level":"REVIEW","risky":true}
意思就是告诉我们触发了人工介入
的风控等级需要处理,这并不影响我们先对后缀
参数加密先展开分析

上面我们把它的整个发包请求的参数转为Python
代码可以看到,除了FECU
这个参数是加密的外,还有它提交的data
参数内有一个udidtk
这个参数看起来也不像是固定的(会不会是沿用了Web的流程什么接口请求下发的
)先不管它!另外就是checkToken
这个参数,它是过了顶像
的滑块给的,携带后方可正常获取到数据
3. 加密分析
这边跟Web
端一样直接关键词参数全局搜索是无法找到有效信息的,还是通过XHR
断点及跟栈分析,直接找到一个OB
混淆的JS文件。遇到这种采取的关键步骤可以先尝试反混淆
一下,AST
算是目前处理混淆、函数变量重命名、多层级的level、控制流扁平化...
主流有效的也是比较有优势的方法
这里我们也可以先使用工具来大致的反混淆一下,作用不大但不至于没用。可以看到JS
代码中明显存在的一些检测项(浏览器环境、自动化还有调试检测
)当然也是可以获取到一些跟加密相关的关键信息。比如MD5
跟AES
的加密。如下所示:
它上图的这个MD5
调用了很多次,加密的对象有环境信息、也有一些动态固定的盐或字符。然后做完多次的MD5
就会得到很多段字符串然后加了时间戳做了一个拼接得到了一个120
多位的长串,然后对这个120+
的字符串进行最后的一个AES
加密。如下所示:
参数最终的长度是194
位,根据上面的分析来总结一下它这个参数的计算加密流程,如下所示:
- 对一个
162
位长度的字符串(盐
)做了MD5
(这个长串几乎每天都会随这个JS
动态更新) - 对
16
位的数字盐进行了MD5
(如上动态) - 对
32
位的动态字符串进行MD5
(动态) - 上面串起来以后末尾追加一个一个数字(动态)
13
位的一个时间戳- 动态混淆JS生成的
5
位字符串
上面所有通过动态加密生成出来的值拼接成上面调试看到的120
多位参与最终AES
加密的长字符串,AES
加密生成出来的字符串是只有192
位的,最终的194
位是因为在做完AES
以后在字符串的头尾部再次各插入了一个字符得到最终的加密值
这里来说一下最后那个动态JS
混淆代码生成的5
位字符是怎么来的,生成位置如下所示:

javascript
function a0_0x3ac913() {
var a0_0x4ee28f = {
_0x55f800: 0x3dd,
_0xd300b0: 0x3ce,
_0xbb5872: 0x35c,
_0x25e711: 0x21e,
_0x5835c1: 0x1af,
_0x27bcc8: 0x2df,
_0x35310c: 0x3dd,
_0x389de6: 0x3ab,
_0x2c9188: 0x58f,
_0x3665e1: 0x328,
_0x25f5b9: 0x329,
_0x318c38: 0x21e,
_0x59f07d: 0x329,
_0x56c922: 0x3f7
}
, _0x76df49 = a0_0x5e6a13
, _0x415cf6 = {};
_0x415cf6[_0x76df49(a0_0x4ee28f._0x55f800)] = _0x76df49(a0_0x4ee28f._0xd300b0),
_0x415cf6[_0x76df49(a0_0x4ee28f._0xbb5872)] = function(_0x2ea17a, _0xde9bd7) {
return _0x2ea17a * _0xde9bd7;
}
,
_0x415cf6[_0x76df49(a0_0x4ee28f._0x25e711)] = function(_0x2abe69, _0x1f0d5e) {
return _0x2abe69 + _0x1f0d5e;
}
,
_0x415cf6[_0x76df49(a0_0x4ee28f._0x5835c1)] = _0x76df49(a0_0x4ee28f._0x27bcc8);
var _0x484dd3 = _0x415cf6;
try {
var _0x54fa41 = 0x5
, _0x213abd = _0x484dd3[_0x76df49(a0_0x4ee28f._0x35310c)]
, _0xcf844 = _0x213abd[_0x76df49(a0_0x4ee28f._0x389de6)]
, _0x8eaac = '';
for (var _0x3bac47 = 0x0; _0x8eaac[_0x76df49(a0_0x4ee28f._0x389de6)] < _0x54fa41; _0x3bac47++) {
var _0xfcf239 = Math[_0x76df49(a0_0x4ee28f._0x2c9188)](_0x484dd3[_0x76df49(a0_0x4ee28f._0xbb5872)](Math[_0x76df49(a0_0x4ee28f._0x3665e1)](), _0xcf844));
if (!_0x213abd[_0x76df49(a0_0x4ee28f._0x25f5b9)]('')[_0xfcf239])
continue;
_0x8eaac = _0x484dd3[_0x76df49(a0_0x4ee28f._0x318c38)](_0x8eaac, _0x213abd[_0x76df49(a0_0x4ee28f._0x59f07d)]('')[_0xfcf239]);
}
return _0x8eaac;
} catch (_0x4e07db) {
return console[_0x76df49(a0_0x4ee28f._0x56c922)](_0x484dd3[_0x76df49(a0_0x4ee28f._0x5835c1)], _0x4e07db),
'';
}
}
来稍微看一下上面的混淆代码做了什么操作,内部根据上面分析,直接对分析出来的加密流程进行代码还原,a0_0x5e6a13
是一个解码的函数,解混淆后可以看到里面包含了62
个字符!循环了5
次来索引生成5
位字符,通过Math.floor(Math.random() * 62)
生成0
-61
之间的随机数,从62
位字符中选取字符,具体代码如下所示:
javascript
function a0_0x4ca3(_0x333150, _0x2c8f46) {
var _0x343f67 = a0_0x13be();
return a0_0x4ca3 = function(_0x4126d9, _0xba068e) {
_0x4126d9 = _0x4126d9 - 0x181;
var _0x13be7 = _0x343f67[_0x4126d9];
if (a0_0x4ca3['JsCqYZ'] === undefined) {
var _0x4ca322 = function(_0x3806ad) {
var _0x258353 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';
var _0x35ed5b = ''
, _0x3b8797 = ''
, _0x49fd92 = _0x35ed5b + _0x4ca322;
for (var _0xd393bf = 0x0, _0x1cec02, _0x4bf15c, _0x5a98b3 = 0x0; _0x4bf15c = _0x3806ad['charAt'](_0x5a98b3++); ~_0x4bf15c && (_0x1cec02 = _0xd393bf % 0x4 ? _0x1cec02 * 0x40 + _0x4bf15c : _0x4bf15c,
_0xd393bf++ % 0x4) ? _0x35ed5b += _0x49fd92['charCodeAt'](_0x5a98b3 + 0xa) - 0xa !== 0x0 ? String['fromCharCode'](0xff & _0x1cec02 >> (-0x2 * _0xd393bf & 0x6)) : _0xd393bf : 0x0) {
_0x4bf15c = _0x258353['indexOf'](_0x4bf15c);
}
for (var _0x5f2dc5 = 0x0, _0xd9179c = _0x35ed5b['length']; _0x5f2dc5 < _0xd9179c; _0x5f2dc5++) {
_0x3b8797 += '%' + ('00' + _0x35ed5b['charCodeAt'](_0x5f2dc5)['toString'](0x10))['slice'](-0x2);
}
return decodeURIComponent(_0x3b8797);
};
a0_0x4ca3['dMwUfl'] = _0x4ca322,
_0x333150 = arguments,
a0_0x4ca3['JsCqYZ'] = !![];
}
var _0x5877e2 = _0x343f67[0x0]
, _0x3fe196 = _0x4126d9 + _0x5877e2
, _0x5dca38 = _0x333150[_0x3fe196];
if (!_0x5dca38) {
var _0xb0f2b1 = function(_0x565615) {
this['yRUkPV'] = _0x565615,
this['aAtqKx'] = [0x1, 0x0, 0x0],
this['mmhcrU'] = function() {
return 'newState';
}
,
this['HZoHkF'] = '\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*',
this['BEzgTG'] = '[\x27|\x22].+[\x27|\x22];?\x20*}';
};
_0xb0f2b1['prototype']['KzabiP'] = function() {
var _0xc855bd = new RegExp(this['HZoHkF'] + this['BEzgTG'])
, _0x3186d8 = _0xc855bd['test'](this['mmhcrU']['toString']()) ? --this['aAtqKx'][0x1] : --this['aAtqKx'][0x0];
return this['Zwynsy'](_0x3186d8);
}
,
_0xb0f2b1['prototype']['Zwynsy'] = function(_0x448299) {
if (!Boolean(~_0x448299))
return _0x448299;
return this['iTzDTy'](this['yRUkPV']);
}
,
_0xb0f2b1['prototype']['iTzDTy'] = function(_0x43ac0b) {
for (var _0x445e27 = 0x0, _0x3f4543 = this['aAtqKx']['length']; _0x445e27 < _0x3f4543; _0x445e27++) {
this['aAtqKx']['push'](Math['round'](Math['random']())),
_0x3f4543 = this['aAtqKx']['length'];
}
return _0x43ac0b(this['aAtqKx'][0x0]);
}
,
new _0xb0f2b1(a0_0x4ca3)['KzabiP'](),
_0x13be7 = a0_0x4ca3['dMwUfl'](_0x13be7),
_0x333150[_0x3fe196] = _0x13be7;
} else
_0x13be7 = _0x5dca38;
return _0x13be7;
}
,
a0_0x4ca3(_0x333150, _0x2c8f46);
}
可以看到上面的解码JS
内有一个方法a0_0x13be
,里面包含了大量的字符串。这种一般都算是整个混淆体系里面的核心数据,可能会包括(加解密的密钥
、函数变量名
)
并通过定义了一个function
来return _0x649c32
重新定义为直接返回字符串数组的函数来隐藏原始函数防止静态分析或者工具直接获取到字符串的内容,如下所示:
javascript
function a0_0x13be() {
var _0x649c32 = ['zMLSBa', 'ANDLCM0', ..., 'yw4T']; // 包含数百个字符串的数组
a0_0x13be = function() {
return _0x649c32;
};
return a0_0x13be();
}
至此,上述分析可以还原出后缀参数
的加密。然后接着看一下data
参数内的udidtk
参数是怎么来的,因为它是动态的不管它校不校验(这种参数最好分析一下JS跟接口层面还原动态获取或生成流程
)有的平台一般的就会通过某些动态风控参来埋点

通过上图查询的发包流程大致是这样:先获取udidtk参数
-->第一次请求
-->验证码拦截
-->过验证码
-->第二次请求
-->拿到数据
所以udidtk
在请求数据接口之前之前携带FUCE
加密参数请求udid/c.do
这个接口拿到就行。最后我们根据上面分析梳理的流程封装加密算法跟请求示例,来请求验证一下,如下所示:
可以看到查询请求失败了,出现了行为验证码
的风控,跟我们页面访问发包接口出现了一样的情况。它这个M
端不管页面还是接口请求访问都是要过这个顶像
验证码的,下面我们来分析一下验证码,对接上去看看是否可以正常请求成功
4. 风控分析
它的这个验证码采用的还是顶像
的多套组合,要是想完全解决多套验证的方案,需要花费比较多的时候来逆向验证码协议,目前看是3
套(滑块还原
、图标点选
、单旋转
)现在好多平台都搞了多套这种机制,从防护角度
来说确实有一定的效果(对于爬虫方来说会很累,比较好的防护厂商的方案都是更新很频繁的
)

这里作者为了验证让文章更加完整,花了点时间就搞一个滑块还原
来验证上面的流程!因为这个貌似频率比较高,如果要搞几种的话我都不想写了(喜欢没有强度的工作
),过掉滑块验证以后拿到CheckToken
参数就可以成功。最终测试一下整个查询航班信息的流程包括过顶像
行为验证的结果,如下所示:
