篇幅有限
完整内容及源码关注公众号:ReverseCode,发送 冲
js语法
作用域
局部变量
1 | function jb(){ |
全局变量
1 | var a = "全局变量" |
自执行函数
js加载自动运行
1 | !(function(){ |
外部调用内部函数
1 | var _hex_md5; |
改成自执行
1 | var _hex_md5; |
很多情况函数定义在对象中
1 | var _hex_md5; |
改成自执行
1 | var _hex_md5; |
define函数自动创建执行,但是纯js环境没有该函数。该函数调用jquery定义函数md5
的js将后面的内容函数命名为md5
。扣代码时创建对象变量var md5;
,原define改成自执行函数!(function(){})()
,其中j
对象中包含需要调用的md5:function(s)
,修改导出语法module.exports=j
为md5=j
赋值为新定义的对象,即可使用md5.md5("123")
调用扣出想要的js函数。
浏览器环境BOM
补头,脱离浏览器在外部不可被直接调用
1 | window = { // window是全局变量 |
html渲染环境DOM
js引擎自带(v8)
1 | document = { // document是全局变量,hook掉原有方法 |
- 点击鼠标 登录
- 网页加载 浏览器指纹(检测是否同一用户),收集是否为浏览器环境
- 图像加载 浏览器指纹 滑块图片还原
- 鼠标移动 浏览器指纹 无感验证
1 | var p = document.createElement("p") |
实战
Preserve log 禁止清除缓存
Disable cache 禁止缓存
ctrl+shift+f 搜索password找到对应js,左下角format格式化,排除vue,axios,jquery
16位 32位加密串 md5
40位加密串 sha1
通过乐易编程助手实现各种加密方案
定位
搜索关键参数
password:
,password=
,password =
请求url,搜索方法var submit
或者function submit
或者submit:
dom元素事件监听,Remove准确定位触发js位置
xhr断点,定位发包函数跟栈,复制网址请求路径到Sources下的XHR/fetch Breakpoints,支持正则。
Network下的发包请求的Initiator,如jquery堆栈的顶层断点(可能会请求多次,找到发包请求时进入的断点),重新请求找到堆栈中属于目标网站的js格式化断点。
fiddler 版本必须 >= v4.6.3,复制
Fiddler 编程猫专用插件
到fiddler程序目录下的Scripts目录中示例:C:\Program Files (x86)\Fiddler2\Scripts
- 覆盖原函数
1 | function xxx(){ |
- Object.defineProperty替换对象属性(getter.setter)
1 | (function () { |
- hook的时机在控制台注入的hook,刷新网页就失效了,过滤Network的js找到第一个加载的js,右键Open in Sources panel格式化,第一行断点,不过有些cookie可能异步可能在html中js生成,在控制台中注入以上hook,清除cookie,手动注入hook,控制台中找到VM虚拟机找到我们的hook的js打上断点,,每次hook都会经过set,右侧就可以查看调用栈,追溯cookie的来源与加密方式。(有可能注入hook的时机会晚于部分异步请求或者html中的js)
- 利用fiddler代理所有请求替换响应,编程猫专用工具注入hook
1 | (function () { |
接下来查看调用栈,最终保存到window.name中。
1 | (function () { |
重新进入iqiyi,断点完成hook定位。
中烟新商盟
元素监听定位,如果出现多个js,尝试Remove再点击看是否报错触发登录定位到事件最终触发的js,尽可能将多个地方打上断点
直接找到对应js位置,先左下角format格式化,ctrl+shift+f打开js页面的控制台,在js页面ctrl查看所有变量值
鼠标悬停,或者控制台打印出来,点击进入方法声明时打上断点,为同一行中的函数打上断点,F8单步调试
to8to
抓包https://www.to8to.com/new_login.php
,在Element面板中搜索new_login.php
ctrl+shift+f 搜索loginCheck
该方法中jq('#rsa_userNum').val(rsaString(password));
,调用rsaString方法加密密码
1 | function rsaString(str) { |
进入encryptfun
定义的js中,rsa加密最少2000行,该方法不过163行,拷贝该js通过编程猫专用工具中的JS调试工具,加载代码,报错引用错误: window 未定义
,添加var window = this;
,报错引用错误: JSEncrypt 未定义
,添加原js中var JSEncrypt = JSEncryptExports.JSEncrypt;
,报错引用错误: JSEncryptExports 未定义
,搜索var JSEncryptExports
,将var JSEncryptExports = {};
添加到JS调试工具,报错类型错误: JSEncrypt is not a constructor
尝试打上断点,但是每次都不能进入断点,说明肯定是动态加载的js,且每次刷新js后缀会有时间戳。勾选Disable cache,打开fiddler抓包,将js拷贝到本地实现http欺骗,选中该请求点击AutoResponder-Add Rule下拉选择Find a File,找到本地保存的js并开启规则
由于每次js请求地址不一样,使用正则匹配regex:https://static\.to8to\.com/gb_js/to8torsaszb\.js\?_=\d+
并保存规则重新发起请求https://static.to8to.com/gb_js/to8torsaszb.js?_=1628128571412
,使用本地js欺骗网络请求js
将整个js格式化找到之前报错JSEncrypt is not a constructor
是从上面的压缩的js中export出来的
将上面压缩的代码添加到编程猫的JS调试工具中加载代码,报错引用错误: navigator 未定义
,添加var navigator = {}
,报错引用错误: window 未定义
,添加var window =this
,因为如果用window ={}
报错ASN1 未定义
,而用this则可以拿到当前js中所有的变量函数。
升学e网通
登录抓包,打开Initiator,进入堆栈顶层定位的代码行
打上断点,查看右侧调用栈,逐个方法往底层去调用,直到react库js找到了preLogin,找到出现password的位置,打上断点
再次登录时,进入断点,找到password
进入加密方法中,aes加密
打开WT-JS中的Crypto类复制key和iv,输出以HEX的十六进制格式,对比结果是标准的AES加密。
基于base64或十六进制的AES加解密实现见aes.js
长房集团
搜索j_password
后打断点,重新登录
进入desEncrypt
中,大致加密完成逻辑就在该函数中
加密逻辑中首先根据SECURITYKEY.get()
获取到key,首先通过请求后端拿到str,判断加密类型是否为aes后截取字符串通过toHexString
转成十六进制拿到key和iv和security
整理完逻辑扣出js报错CryptoJS is not defined
,点击进入 CryptoJS.AES.encrypt
扣出来源码,完整见changfang.js
webpack
遇到webpack的RSA站无脑搜setPublicKey
,遇到定义变量时n(“…”),无脑跳到n的js中把RSA的头部拷贝出来
拼装成如下模板,将含有setPublicKey的js最外层的{}包括起来的函数拷贝到上面的函数定义中,并导出函数名
1 | var _c; // 定义变量 |
拷贝到Sources中的Snippets中自动加载
获取publickey的时候,打上断点,在setPublicKey时使用
添加换行后,完成解密
G妹游戏
首先搜索password
发现出现的点太多,尝试使用initiator的第一个js位置
格式化后直接下断点,直接过滤掉jquery库,查看网站自己的js代码并打上断点,在第二次时才进入真实的发送登录请求的send方法
webpack特征格式
1 | function(x) { |
上面选择方法,下面定义方法。解决webpack框架可以先找到加载模块的方法,找到调用的模块,构造一个自执行方法
![GIF 2021-8-14 21-37-22](JS逆向之调试反爬hook加密技巧/GIF 2021-8-14 21-37-22.gif)
1 | !function(i) { |
以上完成了找到加载模块的方法,构造一个自执行方法。接下来查看加密password
的地方
进入g.encode方法中,想必完整的加密逻辑存在this.jsencrypt.encrypt(i)
中
打上断点后重新登录,进入该断点时,进入jsencrypt.encrypt
,发现存在于function(t, e, i)
中
![GIF 2021-8-14 21-49-44](JS逆向之调试反爬hook加密技巧/GIF 2021-8-14 21-49-44.gif)
将整个function(t, e, i)
(包含qe.prototype.encrypt = function(t) {
的password加密方法)拷贝下来,并定义函数名为jsencrypt
,完成调用模块的扣取。
1 | !function(i) { |
该加密函数在外层被调用,需要在webpack模板中引入
1 | !function(i) { |
var s = r(3);
中的r是function模块传入的第三个参数,由于模块在加载器加载call时调用,不论e.exports,e都不是方法,n是方法且只有一个参数,所以r=n,将加载函数传给了模块,让模块也有了加载的功能。将r(3)
改为r("jsencrypt")
1 | var _n; // 导出n加载器 |
调用_n("jiami")
返回一个对象,prototype
为对象新增方法属性,所以var a = (new _n("jiami"))
,a对象可以调用encode方法
1 | function getKey(pass,time) { |
放到fd中运行报错navigator未定义
,定义var navigator={}
,报错windows未定义
,定义var window=this
,开始运行getKey("123456","1628957324")
完成password加密逻辑
极电竞比分网
看起来被URL编码过,解码后2N1SAJY4LOPJc8gB4WeLYBVD/iB905uUz3VuZjOHMeo=
,看着像base64,在base64的js原生实现中var base64hash = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
,可以在整个网站搜该特征码
Base64是以下(AES DES RSA)算法的实现,这几个算法都和base64有关,定位到base64后可以逐步确认关键算法。
利用XHR定位法,格式化该js后打上断点
逐个调用栈查看参数信息,判断sign生成的具体位置
直到追溯到异步调用栈时最后一步,参数中都没有出现sign的值,点击单步进入逐步调试,找到出现sign值的时机的上一调用栈
回过头来关注n = n.then(e.shift(), e.shift())
,异步执行e中的方法并删除,尝试通过e数组逐个方法进入js的实现
e数组的第一个js方法中找到了sign的赋值位置
一般then前明文,结束后密文,需要格外关注then异步调用时的函数做了什么处理。
观察n字符串的words
数组表示已经加密完成,sigBytes为32位,一个int占4字节,长度是8位,4*8=32,有可能是md5。 n = i()(t)
观察该js源码中i定义为i = n.n(a)
,而a = n(156)
,上面再也找不到n的定义点了,打上断点。
进入n的函数定义,webpack格式雏形出现
将整个js拷贝下来扣除无用代码并整理成如下格式
1 | !function(e) { |
回到网页,继续在return e[t].call(r.exports, r, r.exports, d)
打上断点,通过控制台打印获取a = n(156)
方法
点击进去e[156]
,将该方法拷贝出来到webpack模板中
由于其中有n(148)
,修改为n("_148")
,n就是webpack定义的函数自己d,通过正则匹配该js中是否有其他n调用的函数,n\(\d+)
,不存在其他调用处。在控制台中输入e[148]
,将该方法拷贝出来到webpack模板中
回到上面i = n.n(a)
,断点进入发现就是返回参数a自己,脱裤子放屁
导出webpack模板的方法
将该js复制到浏览器中运行
接下来就是扣取encodeURIComponent(s.a.stringify(n)))
同理,再去return e[t].call(r.exports, r, r.exports, d)
寻找157方法,将代码拷贝到webpack模板中
对比之前的加密逻辑
1 | function getSign(param){ |
总结s.a.stringify(n))
,就是就是上面脱裤子放屁的方法,s.a就是s自己。
推推99
方法定义一般分成三种
- var JSEncrypt = function(a){} 搜索
var JSEncrypt
- JSEncrypt: funtion(a){} 搜索
JSEncrypt:
- JSEncrypt.prototype.encrypt = function(a) {} 搜索
.JSEncrypt
该登录将跳出滑块,最终请求check_login.html?c=0&random=234
,获取该请求的initiator
调用栈,打上断点后重新登录找到上两层调用栈中l_submit
进入该加密方法,并将jsencrypt.min.js:formatted
该加密文件拷贝到fd中运行,报错navigator 未定义
,添加var navigator = {}
,报错window 未定义
,添加var window = this
,添加调用方法
通过断点debug获取setPublicKey时塞入的值
1 | // 由于该js通过exports.JSEncrypt = JSEncrypt;导出了该函数,通过new的方式调用该对象中的方法 |
由于该自执行函数exports
=JSEncryptExports
,所以通过JSEncryptExports.name
即可拿到aa
1 | var JSEncryptExports = {} |
故而可以在最上面定义var jiami;
,在var JSEncrypt = function(a)
之后加入jiami = JSEncrypt
导出对象
1 | function getP(password){ |
拍拍贷
看起来像是RSA,所以搜索setpublicKey
,混淆的话可能搜不到。
图中一个加密方法,一个解密方法,在加密方法的地方断点,找到调用栈上一层checkImgValidateCode
,定位到加密位置
可以找到该段js最上面把加载器拷贝出来,按照之前的解决webpack模板方案解决或者通过系统网页js环境模板.txt
拼接js解决。
本文将通过最原始的方案,首先将.prototype.decrypt = function
所在js全部抠出来,在notepad++中语言选择JavaScript,视图中选择折叠所有层次,搜索加密代码return u(this.getKey().encrypt(t))
,将本段function扣取出来
1 | var jsss = function(t, e, n) { |
报错Cannot read property 'call' of undefined
,查看报错的位置
在追究n(65),n(110)之前,下面是一个自执行函数,n=0,r=function(t){…},而r(e)
中的e作为参数传到var jsss = function(t, e, n)
中交给e[r].call(o.exports,o,o.exports,t),
调用e[0].call传参数,其中的e作为第二个参数,即o.exports,即{}
所以上面代码无效,自执行函数主要执行function(t) {...}
并把e传入处理。
接下来将function(e){…}抠出来并修改为自执行并传入e!(function(t) {...})({})
,放到控制台中运行报错Uncaught ReferenceError: p is not defined
去掉p.default ||
重新运行
断点debug获取到publicKey后开启加密
1 | var aaa = new window.JSEncrypt |