JS逆向之调试反爬hook加密技巧

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

js语法

作用域

局部变量

1
2
3
4
function jb(){
var a = "局部变量"
return a
}

全局变量

1
2
3
4
5
var a = "全局变量"
function qj(){
console.log(a)
}
console.log(a)

自执行函数

js加载自动运行

1
2
3
!(function(){
console.log(2)
})()

外部调用内部函数

1
2
3
4
5
6
7
8
9
var _hex_md5;
function xx(){
var hex_md5 = function(){
console.log("111")
}
_hex_md5 = hex_md5
}
xx()
_hex_md5()

改成自执行

1
2
3
4
5
6
7
8
var _hex_md5;
!(function xx(){
var hex_md5 = function(){
console.log("111")
}
c = hex_md5
})()
_hex_md5()

很多情况函数定义在对象中

1
2
3
4
5
6
7
8
9
10
var _hex_md5;
function xx(){
var j = {
hex_md5: function(){
console.log("111")
}
}
_hex_md5=j;
}
想要调用时需要先调用外部方法赋值再调用j中的hex_md5

改成自执行

1
2
3
4
5
6
7
8
9
10
var _hex_md5;
!(function xx(){
var j = {
hex_md5: function(){
console.log("111")
}
}
_hex_md5=j;
})()
_hex_md5.hex_md5()

define函数自动创建执行,但是纯js环境没有该函数。该函数调用jquery定义函数md5的js将后面的内容函数命名为md5。扣代码时创建对象变量var md5;,原define改成自执行函数!(function(){})(),其中j对象中包含需要调用的md5:function(s),修改导出语法module.exports=jmd5=j赋值为新定义的对象,即可使用md5.md5("123")调用扣出想要的js函数。

浏览器环境BOM

补头,脱离浏览器在外部不可被直接调用

1
2
3
4
5
window = {  // window是全局变量
location:{
href: "chrome://newtab/"
}
}

html渲染环境DOM

js引擎自带(v8)

1
2
3
4
5
6
document = { // document是全局变量,hook掉原有方法
write:function(){

}
}
document.write.toString = function(){return "function write(){ [native code] }"}]
  • 点击鼠标 登录
  • 网页加载 浏览器指纹(检测是否同一用户),收集是否为浏览器环境
  • 图像加载 浏览器指纹 滑块图片还原
  • 鼠标移动 浏览器指纹 无感验证
1
2
3
var p = document.createElement("p")
p.className = "onejane"
document.body.appendChild(p)

实战

Preserve log 禁止清除缓存
Disable cache 禁止缓存
ctrl+shift+f 搜索password找到对应js,左下角format格式化,排除vue,axios,jquery

16位 32位加密串 md5

40位加密串 sha1

通过乐易编程助手实现各种加密方案

image-20210803213139247

定位

  • 搜索关键参数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. 覆盖原函数
1
2
3
4
5
6
7
8
9
10
function xxx(){
console.log("1111")
}
var xxx_ = xxx;
xxx = function(){
console.log("2222")
}
window.alert = function(){console.log("?")}
console.clear = function(){console.log("?")}
setInterval = function(){}
  1. Object.defineProperty替换对象属性(getter.setter)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function () {
var a = "";
Object.defineProperty(document, 'cookie', {
set: function (val) {
console.log('Hook捕获到cookie设置->', val);
a = val;
return val;
},
get: function(){
return a;
}
});
})();
document.cookie = "1" // 设置
document.cookie // 获取
  1. hook的时机在控制台注入的hook,刷新网页就失效了,过滤Network的js找到第一个加载的js,右键Open in Sources panel格式化,第一行断点,不过有些cookie可能异步可能在html中js生成,在控制台中注入以上hook,清除cookie,手动注入hook,控制台中找到VM虚拟机找到我们的hook的js打上断点,,每次hook都会经过set,右侧就可以查看调用栈,追溯cookie的来源与加密方式。(有可能注入hook的时机会晚于部分异步请求或者html中的js)

image-20210804080959449

  1. 利用fiddler代理所有请求替换响应,编程猫专用工具注入hook

image-20210804083128963

1
2
3
4
5
6
7
8
9
10
11
12
(function () {
'use strict';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf("__dfp") != -1) {
debugger;
}
console.log('Hook捕获到cookie设置->', val);
return val;
}
});
})();

image-20210804083140943

接下来查看调用栈,最终保存到window.name中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function () {
'use strict';
var a = "";
Object.defineProperty(window, 'name', {
set: function (val) {
debugger;
a = val;
console.log('Hook捕获到cookie设置->', val);
return val;
},
get: function(){
return a;
}
});
})();

重新进入iqiyi,断点完成hook定位。

中烟新商盟

元素监听定位,如果出现多个js,尝试Remove再点击看是否报错触发登录定位到事件最终触发的js,尽可能将多个地方打上断点

image-20210803083951861

直接找到对应js位置,先左下角format格式化,ctrl+shift+f打开js页面的控制台,在js页面ctrl查看所有变量值

image-20210803084857034

鼠标悬停,或者控制台打印出来,点击进入方法声明时打上断点,为同一行中的函数打上断点,F8单步调试

image-20210803085611483

image-20210803085804754

to8to

抓包https://www.to8to.com/new_login.php,在Element面板中搜索new_login.php

image-20210804085008500

ctrl+shift+f 搜索loginCheck

image-20210804085123921

该方法中jq('#rsa_userNum').val(rsaString(password));,调用rsaString方法加密密码

1
2
3
function rsaString(str) {
return encodeURIComponent(RSAUtilszb.encryptfun(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

image-20210804144205977

尝试打上断点,但是每次都不能进入断点,说明肯定是动态加载的js,且每次刷新js后缀会有时间戳。勾选Disable cache,打开fiddler抓包,将js拷贝到本地实现http欺骗,选中该请求点击AutoResponder-Add Rule下拉选择Find a File,找到本地保存的js并开启规则

image-20210805091834690

由于每次js请求地址不一样,使用正则匹配regex:https://static\.to8to\.com/gb_js/to8torsaszb\.js\?_=\d+并保存规则重新发起请求https://static.to8to.com/gb_js/to8torsaszb.js?_=1628128571412,使用本地js欺骗网络请求js

image-20210805100020494

image-20210805100413163

将整个js格式化找到之前报错JSEncrypt is not a constructor是从上面的压缩的js中export出来的

image-20210805133854931

将上面压缩的代码添加到编程猫的JS调试工具中加载代码,报错引用错误: navigator 未定义,添加var navigator = {},报错引用错误: window 未定义,添加var window =this,因为如果用window ={}报错ASN1 未定义,而用this则可以拿到当前js中所有的变量函数。

image-20210805150052429

升学e网通

登录抓包,打开Initiator,进入堆栈顶层定位的代码行

image-20210805161322607

打上断点,查看右侧调用栈,逐个方法往底层去调用,直到react库js找到了preLogin,找到出现password的位置,打上断点

image-20210805162037531

再次登录时,进入断点,找到password

image-20210805190251645

进入加密方法中,aes加密

image-20210805190344051

打开WT-JS中的Crypto类复制key和iv,输出以HEX的十六进制格式,对比结果是标准的AES加密。

image-20210805191253856

基于base64或十六进制的AES加解密实现见aes.js

image-20210805225410622

长房集团

image-20210806082118227

搜索j_password后打断点,重新登录

image-20210806082341365

进入desEncrypt中,大致加密完成逻辑就在该函数中

image-20210806082652167

加密逻辑中首先根据SECURITYKEY.get()获取到key,首先通过请求后端拿到str,判断加密类型是否为aes后截取字符串通过toHexString转成十六进制拿到key和iv和security

image-20210806083259472

整理完逻辑扣出js报错CryptoJS is not defined,点击进入 CryptoJS.AES.encrypt扣出来源码,完整见changfang.js

image-20210806085244757

webpack

遇到webpack的RSA站无脑搜setPublicKey,遇到定义变量时n(“…”),无脑跳到n的js中把RSA的头部拷贝出来

image-20210825200108200

拼装成如下模板,将含有setPublicKey的js最外层的{}包括起来的函数拷贝到上面的函数定义中,并导出函数名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var _c; // 定义变量
(function (e) {
function c(t) {
// if (i[t]) return i[t].exports;
var n = /*i[t] = */ {
i: t,
l: !1,
exports: {}
};
return e[t].call(n.exports, n, n.exports, c),
n.l = !0,
n.exports
}
_c = c; // 导出函数
})({
"0022" : function(t,e,n){...}
})

拷贝到Sources中的Snippets中自动加载

image-20210825201017211

获取publickey的时候,打上断点,在setPublicKey时使用

image-20210825201231729

添加换行后,完成解密

image-20210825201402072

G妹游戏

image-20210808212024464

首先搜索password发现出现的点太多,尝试使用initiator的第一个js位置

image-20210808212118312

格式化后直接下断点,直接过滤掉jquery库,查看网站自己的js代码并打上断点,在第二次时才进入真实的发送登录请求的send方法

image-20210808214442530

webpack特征格式

1
2
3
4
5
6
7
8
9
10
11
function(x) {
function xx(yy){
x[yy].call(xxx,xxx,xxx); // 必有一个加载模块的方法 call apply
}
}([ // 数组 }([ 对象 }({
function(x1,x2,x3){},
function(x1,x2,x3){},
function(x1,x2,x3){},
function(x1,x2,x3){},
function(x1,x2,x3){}
]);

上面选择方法,下面定义方法。解决webpack框架可以先找到加载模块的方法,找到调用的模块,构造一个自执行方法

![GIF 2021-8-14 21-37-22](JS逆向之调试反爬hook加密技巧/GIF 2021-8-14 21-37-22.gif)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
!function(i) {
var s = {};
function n(t) {
if (s[t])
return s[t].exports;
var e = s[t] = {
i: t,
l: !1,
exports: {}
};
return i[t].call(e.exports, e, e.exports, n),
e.l = !0,
e.exports
}
}()

以上完成了找到加载模块的方法,构造一个自执行方法。接下来查看加密password的地方

image-20210814214034849

进入g.encode方法中,想必完整的加密逻辑存在this.jsencrypt.encrypt(i)

image-20210814214058116

打上断点后重新登录,进入该断点时,进入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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
!function(i) {
var s = {};
function n(t) {
if (s[t])
return s[t].exports;
var e = s[t] = {
i: t,
l: !1,
exports: {}
};
return i[t].call(e.exports, e, e.exports, n),
e.l = !0,
e.exports
}
}(
{
jsencrypt: function(t, e, i) {...}
}
)

该加密函数在外层被调用,需要在webpack模板中引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
!function(i) {
var s = {};
function n(t) {
if (s[t])
return s[t].exports;
var e = s[t] = {
i: t,
l: !1,
exports: {}
};
return i[t].call(e.exports, e, e.exports, n),
e.l = !0,
e.exports
}
}(
{
jsencrypt: function(t, e, i) {...},
jiami: function(t, e, r) { // 其中调用了jsencrypt
var i;
(i = function(t, e, i) {
var s = r(3);
function n() {
void 0 !== s && (this.jsencrypt = new s.JSEncrypt,
this.jsencrypt.setPublicKey("-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB-----END PUBLIC KEY-----"))
}
n.prototype.encode = function(t, e) {
var i = e ? e + "|" + t : t;
return encodeURIComponent(this.jsencrypt.encrypt(i))
}
,
i.exports = n
}
.call(e, r, e, t)) === undefined || (t.exports = i)
}
}
)

var s = r(3);中的r是function模块传入的第三个参数,由于模块在加载器加载call时调用,不论e.exports,e都不是方法,n是方法且只有一个参数,所以r=n,将加载函数传给了模块,让模块也有了加载的功能。将r(3)改为r("jsencrypt")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var _n;  // 导出n加载器
!function(i) {
var s = {};
function n(t) {
if (s[t])
return s[t].exports;
var e = s[t] = {
i: t,
l: !1,
exports: {}
};
return i[t].call(e.exports, e, e.exports, n),
e.l = !0,
e.exports
}
_n = n;
}(
{
jsencrypt: function(t, e, i) {...},
jiami: function(t, e, r) { // 其中调用了jsencrypt
var i;
(i = function(t, e, i) {
var s = r("jsencrypt");
function n() {
void 0 !== s && (this.jsencrypt = new s.JSEncrypt,
this.jsencrypt.setPublicKey("-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB-----END PUBLIC KEY-----"))
}
n.prototype.encode = function(t, e) {
var i = e ? e + "|" + t : t;
return encodeURIComponent(this.jsencrypt.encrypt(i))
}
,
i.exports = n
}
.call(e, r, e, t)) === undefined || (t.exports = i)
}
}
)

调用_n("jiami")返回一个对象,prototype为对象新增方法属性,所以var a = (new _n("jiami")),a对象可以调用encode方法

1
2
3
4
5
function getKey(pass,time) {
var jiami_ = _n("jiami")
var _jiami = (new jiami_);
return _jiami.encode(pass,time)
}

放到fd中运行报错navigator未定义,定义var navigator={},报错windows未定义,定义var window=this,开始运行getKey("123456","1628957324")完成password加密逻辑

极电竞比分网

image-20210815102936925

看起来被URL编码过,解码后2N1SAJY4LOPJc8gB4WeLYBVD/iB905uUz3VuZjOHMeo=,看着像base64,在base64的js原生实现中var base64hash = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',可以在整个网站搜该特征码

image-20210815103351898

Base64是以下(AES DES RSA)算法的实现,这几个算法都和base64有关,定位到base64后可以逐步确认关键算法。

利用XHR定位法,格式化该js后打上断点

image-20210819081833996

逐个调用栈查看参数信息,判断sign生成的具体位置

image-20210819082005121

直到追溯到异步调用栈时最后一步,参数中都没有出现sign的值,点击单步进入逐步调试,找到出现sign值的时机的上一调用栈

image-20210819083200540

回过头来关注n = n.then(e.shift(), e.shift()),异步执行e中的方法并删除,尝试通过e数组逐个方法进入js的实现

image-20210829170243059

e数组的第一个js方法中找到了sign的赋值位置

image-20210819084149988

一般then前明文,结束后密文,需要格外关注then异步调用时的函数做了什么处理。

image-20210822070517372

观察n字符串的words数组表示已经加密完成,sigBytes为32位,一个int占4字节,长度是8位,4*8=32,有可能是md5。 n = i()(t)观察该js源码中i定义为i = n.n(a),而a = n(156),上面再也找不到n的定义点了,打上断点。

image-20210822070908891

进入n的函数定义,webpack格式雏形出现

image-20210822071025882

将整个js拷贝下来扣除无用代码并整理成如下格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
!function(e) {
var n = {}
function d(t) {
if (n[t])
return n[t].exports;
var r = n[t] = {
i: t,
l: !1,
exports: {}
};
return e[t].call(r.exports, r, r.exports, d),
r.l = !0,
r.exports
}
}({});

回到网页,继续在return e[t].call(r.exports, r, r.exports, d)打上断点,通过控制台打印获取a = n(156)方法

image-20210822071633798

点击进去e[156],将该方法拷贝出来到webpack模板中

image-20210822071728633

由于其中有n(148),修改为n("_148"),n就是webpack定义的函数自己d,通过正则匹配该js中是否有其他n调用的函数,n\(\d+),不存在其他调用处。在控制台中输入e[148],将该方法拷贝出来到webpack模板中

image-20210822072551724

回到上面i = n.n(a),断点进入发现就是返回参数a自己,脱裤子放屁

image-20210822072726472

导出webpack模板的方法

image-20210822073219072

将该js复制到浏览器中运行

image-20210822073138659

接下来就是扣取encodeURIComponent(s.a.stringify(n)))

image-20210822073649573

同理,再去return e[t].call(r.exports, r, r.exports, d)寻找157方法,将代码拷贝到webpack模板中

image-20210822074050159

对比之前的加密逻辑

image-20210822075207657

1
2
3
4
5
6
7
function getSign(param){
// param = timestamp=1629589453737&secret=aHVheWluZ19zZWNyZXRfYXBp
var i = fff("_156") //a = n(156), i = n.n(a) 获取到了i变量
var n = i(param) // i()(t)
var s = fff("_157") // c = n(157), s = n.n(c)
return s.stringify(n)
}

总结s.a.stringify(n)),就是就是上面脱裤子放屁的方法,s.a就是s自己。

推推99

方法定义一般分成三种

  1. var JSEncrypt = function(a){} 搜索 var JSEncrypt
  2. JSEncrypt: funtion(a){} 搜索JSEncrypt:
  3. JSEncrypt.prototype.encrypt = function(a) {} 搜索 .JSEncrypt

image-20210822153348785

该登录将跳出滑块,最终请求check_login.html?c=0&random=234,获取该请求的initiator调用栈,打上断点后重新登录找到上两层调用栈中l_submit

image-20210822153954411

进入该加密方法,并将jsencrypt.min.js:formatted该加密文件拷贝到fd中运行,报错navigator 未定义,添加var navigator = {},报错window 未定义,添加var window = this,添加调用方法

image-20210822162200737

通过断点debug获取setPublicKey时塞入的值

1
2
3
4
5
6
// 由于该js通过exports.JSEncrypt = JSEncrypt;导出了该函数,通过new的方式调用该对象中的方法
function getP(password){
var encrypt = new JSEncrypt()
encrypt.setPublicKey(...)
return encrypt.encrypt(password)
}

由于该自执行函数exports=JSEncryptExports,所以通过JSEncryptExports.name 即可拿到aa

1
2
3
4
var JSEncryptExports = {}
!(function(exports) {
exports.name = 'aa'
})(JSEncryptExports);

故而可以在最上面定义var jiami;,在var JSEncrypt = function(a)之后加入jiami = JSEncrypt导出对象

1
2
3
4
5
function getP(password){
var encrypt = new jiami()
encrypt.setPublicKey(...)
return encrypt.encrypt(password)
}

拍拍贷

image-20210822163235139

看起来像是RSA,所以搜索setpublicKey,混淆的话可能搜不到。

image-20210822163923945

图中一个加密方法,一个解密方法,在加密方法的地方断点,找到调用栈上一层checkImgValidateCode,定位到加密位置

image-20210822164022300

可以找到该段js最上面把加载器拷贝出来,按照之前的解决webpack模板方案解决或者通过系统网页js环境模板.txt拼接js解决。

本文将通过最原始的方案,首先将.prototype.decrypt = function所在js全部抠出来,在notepad++中语言选择JavaScript,视图中选择折叠所有层次,搜索加密代码return u(this.getKey().encrypt(t)),将本段function扣取出来

image-20210822220414389

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var jsss = function(t, e, n) {
"use strict";
function r(t) {
return t && t.__esModule ? t : {
default: t
}
}
var i, o, a, s = n(65), c = r(s), u = n(110), l = r(u), d = n(111), p = r(d), f = n(114), h = r(f);
!function(n, r) {
"object" == (0,
h.default)(e) && void 0 !== t ? r(e) : (o = [e],
i = r,
void 0 !== (a = "function" == typeof i ? i.apply(e, o) : i) && (t.exports = a))
}(0,
function(t) {...}
}
var e = [jsss]
function t(r) {
var o = {
exports: {},
id: r,
loaded: !1
};
return e[r].call(o.exports,o,o.exports,t),
o.loaded = !0,
o.exports
}
t(0)

报错Cannot read property 'call' of undefined,查看报错的位置

image-20210823082351227

在追究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,即{}

image-20210823083734114

所以上面代码无效,自执行函数主要执行function(t) {...}并把e传入处理。

接下来将function(e){…}抠出来并修改为自执行并传入e!(function(t) {...})({}),放到控制台中运行报错Uncaught ReferenceError: p is not defined

image-20210823084758664

去掉p.default ||重新运行

image-20210823085154409

断点debug获取到publicKey后开启加密

1
2
3
var aaa = new window.JSEncrypt
aaa.setPublicKey("...")
aaa.encrypt("123456")
文章作者: J
文章链接: http://onejane.github.io/2021/08/02/JS逆向之调试反爬hook加密技巧/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏