关注微信公众号:K 哥爬虫,继续分享爬虫进阶、JS/ 安卓逆向等技术干货!
申明
本文章中所有内容仅供学习交换,抓包内容、敏感网址、数据接口均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关,若有侵权,请分割我立刻删除!
逆向指标
- 指标:W 店登录接口 UA 参数加密,JS 代码通过了 OB 混同
- 主页:
aHR0cHM6Ly9kLndlaWRpYW4uY29tLw==
- 接口:
aHR0cHM6Ly9zc28xLndlaWRpYW4uY29tL3VzZXIvbG9naW4=
- 逆向参数:Form Data:
ua: H4sIAAAAAAAAA91ViZUbMQhtiVOIcnRRxRafr%2FGuN5ukgoyfLUZC...
OB 混同简介
OB 混同全称 Obfuscator,Obfuscator 其实就是混同的意思,官网:https://obfuscator.io/,其作者是一位叫 Timofey Kachalov 的俄罗斯 JavaScript 开发工程师,早在 2016 年就公布了第一个版本。
一段失常的代码如下:
function hi() {console.log("Hello World!");
}
hi();
通过 OB 混同后的后果:
function _0x3f26() {var _0x2dad75 = ['5881925kTCKCP', 'Hello\x20World!', '600mDvfGa', '699564jYNxbu', '1083271cEvuvT', 'log', '18sKjcFY', '214857eMgFSU', '77856FUKcuE', '736425OzpdFI', '737172JqcGMg'];
_0x3f26 = function () {return _0x2dad75;};
return _0x3f26();}
(function (_0x307c88, _0x4f8223) {var _0x32807d = _0x1fe9, _0x330c58 = _0x307c88();
while (!![]) {
try {var _0x5d6354 = parseInt(_0x32807d(0x6f)) / 0x1 + parseInt(_0x32807d(0x6e)) / 0x2 + parseInt(_0x32807d(0x70)) / 0x3 + -parseInt(_0x32807d(0x69)) / 0x4 + parseInt(_0x32807d(0x71)) / 0x5 + parseInt(_0x32807d(0x6c)) / 0x6 * (parseInt(_0x32807d(0x6a)) / 0x7) + -parseInt(_0x32807d(0x73)) / 0x8 * (parseInt(_0x32807d(0x6d)) / 0x9);
if (_0x5d6354 === _0x4f8223) break; else _0x330c58['push'](_0x330c58['shift']());
} catch (_0x3f18e4) {_0x330c58['push'](_0x330c58['shift']());
}
}
}(_0x3f26, 0xaa023));
function _0x1fe9(_0xa907e7, _0x410a46) {var _0x3f261f = _0x3f26();
return _0x1fe9 = function (_0x1fe950, _0x5a08da) {
_0x1fe950 = _0x1fe950 - 0x69;
var _0x82a06 = _0x3f261f[_0x1fe950];
return _0x82a06;
}, _0x1fe9(_0xa907e7, _0x410a46);
}
function hi() {
var _0x12a222 = _0x1fe9;
console[_0x12a222(0x6b)](_0x12a222(0x72));
}
hi();
OB 混同具备以下特色:
1、个别由一个大数组或者含有大数组的函数、一个自执行函数、解密函数和加密后的函数四局部组成;
2、函数名和变量名通常以 _0x
或者 0x
结尾,后接 1~6 位数字或字母组合;
3、自执行函数,进行移位操作,有显著的 push、shift 关键字;
例如在下面的例子中,_0x3f26()
办法就定义了一个大数组,自执行函数里有 push、shift 关键字,次要是对大数组进行移位操作,_0x1fe9()
就是解密函数,hi()
就是加密后的函数。
抓包剖析
点击登陆抓包,能够看到有个 ua 参数,通过了加密,每次登陆是会扭转的,如下图所示:
如果间接搜寻 ua 的话,后果太多,不不便筛选,通过 XHR 断点比拟容易找到加密的地位,如下图所示,最初提交的 r 参数蕴含 ua 值,往上找能够看到是 i 的值通过了 URL 编码,再往上看,i 的值通过 window.getUa()
获取,这个实际上是 uad.js 外面的一个匿名函数。
跟进到 uad.js,能够看到调用了 window[_0x4651('0x710')]
这个办法,最初返回的 _0x261229
就是加密后的 ua 值,用鼠标把相似 _0x4651('0x710')
、_0x4651('0x440')
的值选中,能够看到实际上是一些字符串,这些字符串通过间接搜寻,能够发现是在头部的一个大数组里,如下图所示:
混同还原与替换
一个大数组,一个有显著的 push、shift 关键字的进行移位操作的自执行函数,是 OB 混同无疑了,那么咱们应该怎么去解决,让其看起来更悦目一些呢?
你能够手动在浏览器选中查看值,在本地去替换,当然不必全副去替换,跟栈走,用到的中央替换就行了,不要傻傻的全副去挨个手动替换,这种办法实用于不太简单的代码。
如果遇到代码很多的状况,倡议应用反混同工具去解决,这里举荐国内的猿人学 OB 混同专解工具和国外的 de4js,猿人学的工具还原水平很高,然而局部 OB 混同还原后运行会报错,实测本案例的 OB 混同通过猿人学的工具解决后就不能失常运行,可能须要本人事后解决一下才行,de4js 这个工具是越南的一个作者开发的,开源的,你能够部署到本人的机器上,它反对多种混同还原,包含 Eval、OB、JSFuck、AA、JJ 等,能够间接粘贴代码,自动识别混同形式,本案例举荐应用 de4js,如下图所示:
咱们将还原后的后果复制到本地文件,应用 Fiddler 的 Autoresponder 性能对响应进行替换,如下图所示:
如果此时开启抓包,刷新页面,你会发现申请状态 status 显示的是 CORS error,JS 替换不胜利,在控制台里还能够看到报错 No 'Access-Control-Allow-Origin' header is present on the requested resource.
如下图所示:
CORS 跨域谬误
CORS(Cross-Origin Resource Sharing,跨域资源共享)是一个 W3C 规范,该规范应用附加的 HTTP 头来通知浏览器,容许运行在一个源上的 Web 利用拜访位于另一不同源的资源。一个申请 URL 的协定、域名、端口三者之间任意与以后页面地址不同即为跨域。常见的跨域问题就是浏览器提醒在 A 域名下不能够拜访 B 域名的 API,无关 CORS 的进一步了解,能够参考 W3C CORS Enabled。
简要流程如下:
1、消费者发送一个 Origin 报头到提供者端:Origin: http://www.site.com;
2、提供者发送一个 Access-Control-Allow-Origin
响应报头给消费者,如果值为 *
或 Origin 对应的站点,则示意容许共享资源给消费者,如果值为 null 或者不存在,则示意不容许共享资源给消费者;
3、除了 Access-Control-Allow-Origin
以外,局部站点还有可能检测 Access-Control-Allow-Credentials
,为 true 示意容许;
4、浏览器依据提供者的响应报文判断是否容许消费者跨域拜访到提供者源;
咱们依据后面在控制台的报错信息,能够晓得是响应头短少 Access-Control-Allow-Origin
导致的,在 Fiddler 外面有两种办法为响应头增加此参数,上面别离介绍一下:
第一种是利用 Fiddler 的 Filter 性能,在 Response Headers 里设置即可,别离填入 Access-Control-Allow-Origin 和容许的域名,如下图所示:
第二种是批改 CustomRules.js 文件,顺次抉择 Rules —> Customize Rules,在 static function OnBeforeResponse(oSession: Session)
模块下减少以下代码:
if(oSession.uriContains("要解决的 URL")){oSession.oResponse["Access-Control-Allow-Origin"] = "容许的域名";
}
两种办法二选一,设置结束后,就能够胜利替换了,刷新再次调试就能够看到是还原后的 JS 了,如下图所示:
逆向剖析
很显著 window.getUa
是次要的加密函数,所以咱们先来剖析一下这个函数:
window.getUa = function() {var _0x7dfc34 = new Date().getTime();
if (_0x4a9622) {_0x2644f4();
}
_0x55b608();
var _0x261229 = _0x1722c3(_0x2e98dd) + '|' + _0x1722c3(_0x420004) + '|' + _0x7dfc34.toString(0x10);
_0x261229 = btoa(_0x570bef.gzip(_0x261229, {'to': 'string'}));
return _0x261229;
};
_0x7dfc34
是工夫戳,接着一个 if 判断,咱们能够鼠标放到判断里去看看,发现判断的 _0x4a9622
是 false,那么 _0x2644f4()
就不会被执行,而后执行了 _0x55b608()
办法,_0x261229
的值,次要调用了 _0x1722c3()
办法失去的,前后顺次传入了 _0x2e98dd
和 _0x420004
,很显著这两个值比拟要害,别离搜寻一下,能够发现:
_0x2e98dd
定义了一些 header、浏览器的信息、屏幕信息、零碎字体信息等,这些信息能够作为定值间接传入,如下图所示:
_0x420004
搜寻有用的后果就是仅定义了一个空对象,在控制台输入一下能够看到实际上蕴含了一些键盘、鼠标点击挪动的数据,实际上通过测试发现,_0x420004
的值并不是强校验的,能够应用随机数模仿生成,也能够间接复制一个定值。
_0x2e98dd
和 _0x420004
这两个参数都没有进行强校验,齐全能够以定值的形式传入,这两个值都是 JSON 格局,咱们能够间接在控制台应用 copy
语句复制其值,或者应用 JSON.stringify()
语句输入后果再手动复制。
本地联调
外面各个函数互相调用,比拟多,能够间接把整个 JS copy 下来,咱们留神到整个函数是一个自执行函数,在本地调用时,咱们能够定义一个全局变量,而后在 window.getUa
函数里,将 _0x261229
的值赋值给全局变量,也就相当于导出值,最初取这个全局变量即可,还有一种办法就是不让它自执行,改写成失常个别的函数,而后调用 window.getUa
办法失去 ua 值。
首先咱们把 _0x2e98dd
和 _0x420004
的值在本地定义一下,这里有个小细节,须要把原 JS 代码里这两个值定义的中央正文掉,避免起抵触。
在本地调试时,会提醒 window
、location
、document
未定义,定义一下为空对象即可,而后又提醒 attachEvent
未定义,搜寻一下,是 _0x13cd5a
的一个原型对象,除了 attachEvent
以外,还有个 addEventListener
,addEventListener()
办法用于向指定元素增加事件句柄,在 IE 中应用 attachEvent()
办法来实现,咱们在 Google Chrome 外面埋下断点调试一下,刷新页面会间接进入 addEventListener()
办法,其中的事件是 keydown
,即键盘按下,就调用前面的 _0x5cec90
办法,输入一下前面返回的 this,实际上并没有产生什么有用的值,所以 _0x13cd5a.prototype.bind
办法咱们能够间接将其正文掉,理论测试也没有影响。
接着本地调试,又会提醒 btoa
未定义,btoa
和 atob
是 window 对象的两个函数,其中 btoa
是 binary to ascii,用于将 binary 的数据用 ascii 码示意,即 Base64 的编码过程,而 atob
则是 ascii to binary,用于将 ascii 码解析成 binary 数据,即 Base64 的解码过程。
在 NodeJS 里,提供了一个称为 Buffer
的本地模块,可用于执行 Base64 编码和解码,这里不做具体介绍,可自行百度,window.getUa
办法里的原 btoa
语句是这样的:
_0x261229 = btoa(_0x570bef.gzip(_0x261229, {'to': 'string'}));
在 NodeJS 里,咱们能够这样写:
_0x261229 = Buffer.from(_0x570bef.gzip(_0x261229, {'to': 'string'}), "latin1").toString('base64');
留神:Buffer.from()
传入了一个 latin1
参数,这是因为 _0x570bef.gzip(_0x261229, {'to': 'string'})
的后果是 Latin1(ISO-8859-1 的别名)编码,如果不传,或者传入其余参数,则最终后果可能和 btoa
办法得出的后果不一样!
自此,本地联调结束,就能够失去正确的 ua 值了!
残缺代码
GitHub 关注 K 哥爬虫,继续分享爬虫相干代码!欢送 star!https://github.com/kgepachong/
以下只演示局部要害代码,不能间接运行! 残缺代码仓库地址:https://github.com/kgepachong…
JavaScript 加密要害代码架构
var window = {};
var location = {};
var document = {};
var _0x5a577d = function () {}();
var _0xe26ae = function () {}();
var _0x3204b9 = function () {}();
var _0x3c7e70 = function () {}();
var _0x4a649b = function () {}();
var _0x21524f = function () {}();
var _0x2b0d61 = function () {}();
var _0x53634a = function () {}();
var _0x570bef = function () {}();
var _0xd05c32 = function (_0x5c6c0c) {};
window.CHLOROFP_STATUS = 'start';
// 此处省略 N 个函数
var _0x2e98dd = {
// 对象具体的值已省略
"basic": {},
"header": {},
"navigator": {},
"screenData": {},
"sysfonts": [],
"geoAndISP": {},
"browserType": {},
"performanceTiming": {},
"canvasFp": {},
"visTime": [],
"other": {}}
var _0x420004 = {
// 对象具体的值已省略
"keypress": true,
"scroll": true,
"click": true,
"mousemove": true,
"mousemoveData": [],
"keypressData": [],
"mouseclickData": [],
"wheelDeltaData": []}
window.getUa = function () {var _0x7dfc34 = new Date().getTime();
if (_0x4a9622) {_0x2644f4();
}
_0x55b608();
var _0x261229 = _0x1722c3(_0x2e98dd) + '|' + _0x1722c3(_0x420004) + '|' + _0x7dfc34.toString(0x10);
// _0x261229 = btoa(_0x570bef.gzip(_0x261229, {'to': 'string'}));
_0x261229 = Buffer.from(_0x570bef.gzip(_0x261229, {'to': 'string'}), "latin1").toString('base64');
return _0x261229;
};
// 测试输入
// console.log(window.getUa())
Python 登录要害代码
# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-15
# @Author : 微信公众号:K 哥爬虫
# @FileName: weidian_login.py
# @Software: PyCharm
# ==================================
import execjs
import requests
from urllib import parse
index_url = "脱敏解决,残缺代码关注 GitHub:https://github.com/kgepachong/crawler"
login_url = "脱敏解决,残缺代码关注 GitHub:https://github.com/kgepachong/crawler"
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
session = requests.session()
def get_encrypted_ua():
with open('get_encrypted_ua.js', 'r', encoding='utf-8') as f:
uad_js = f.read()
ua = execjs.compile(uad_js).call('window.getUa')
ua = parse.quote(ua)
return ua
def get_wd_token():
headers = {"User-Agent": UserAgent}
response = session.get(url=index_url, headers=headers)
wd_token = response.cookies.get_dict()["wdtoken"]
return wd_token
def login(phone, password, ua, wd_token):
headers = {
"user-agent": UserAgent,
"origin": "脱敏解决,残缺代码关注 GitHub:https://github.com/kgepachong/crawler",
"referer": "脱敏解决,残缺代码关注 GitHub:https://github.com/kgepachong/crawler",
}
data = {
"phone": phone,
"countryCode": "86",
"password": password,
"version": "1",
"subaccountId": "","clientInfo":'{"clientType": 1}',"captcha_session":"",
"captcha_answer": "","vcode":"",
"mediaVcode": "","ua": ua,"scene":"PCLogin","wdtoken": wd_token
}
response = session.post(url=login_url, headers=headers, data=data)
print(response.json())
def main():
phone = input("请输出登录手机号:")
password = input("请输出登录明码:")
ua = get_encrypted_ua()
wd_token = get_wd_token()
login(phone, password, ua, wd_token)
if __name__ == '__main__':
main()