声明:本文只作学习研究,禁止用于非法用途,否则后果自负,如有侵权,请告知删除,谢谢!
前言:我技术可能不是很牛逼,但我很会偷懒,怎么简单怎么来,所以有更好的解密方法可以在评论区评论~
目标网站:这个网站很坑,那个验证字段搜不到,XHR断点也断不到,就很奇怪,不过竟然我敢写这篇文章,就代表我是过了的。
这是网站:
aHR0cHM6Ly90b29scy5taWt1LmFjL25vdmVsYWkv
关于JS逆向,无非就是:
1.寻找参数
2.定位参数生成的位置
3.分析参数的加密逻辑
4.扣代码调用
关于webpack,不了解的可以看下面这篇文章:
https://app.yinxiang.com/fx/970ae39c-9964-4aae-aa96-7e81fee4ef8f
简单来说就是经过webpack打包之后所有资源都变成了模块,通过“加载器(分发器)”进行调用。
下面开始逆向过程:
打开目标网站,多点击目标网站的“开始生成”按钮,等待生成成功,比较几次请求可以发现请求参数都是一样的,而请求头中有个authsign每次都是不一样的,故判断该参数是验证参数(实际上确实是这个)。
第一步,老规矩先全局搜索一下,
似乎是没有赋值的地方,这就很奇怪,
第二步,定位参数位置。总之先看看发起请求的位置(在启动器里面找)(其实还有另一种方法:Js Hook,下面再讲)
跳转到这里了,在这里打个断点。
再次点击“开始生成”
可以发现这里有个t,里面的headers不知道是什么(一般的headers都是请求头,所以这个很可疑,先记住这个t),总之按F10跳过函数继续执行。
e是个数组,好像在遍历这个数组 ,不管他,反正看不懂,接着摁F10跳过。
到这里之后可以发现这里请求参数,但是并没有我们想要的"authsign", 接着摁F10跳过直到看到我们需要的参数(Authsign)。
可以发现这里又出现了那个t,里面的headers依旧是那些值。在摁F10跳过函数
好,跳过之后可以发现这里这个t里面的headers里面多了个Authsign。所以这个Authsign的生成位置就在这里面。
我们在这里打上断点,取消其他断点,重新点击“开始生成”,
在这里断住之后,我们摁F9逐步执行(F10是跨函数执行),
逐步执行到了这里,打印一下,可以发现这里就是加密参数生成的地方,现在已经定位到了。
来讲讲另一种方法:JS Hook
需要在浏览器安装拓展:油猴(Violentmonkey)
打开插件新建脚本:该脚本的作用是在指定请求头被设置的时候进入debugger状态。
更多脚本:(https://www.jb51.net/article/241736.htm)
// ==UserScript==
// @name tools.miku
// @namespace Violentmonkey Scripts
// @match https://*/*
// @grant none
// @version 1.0
// @author -
// @description 2022/11/4 08:39:59
// ==/UserScript==
var code = function(){
var org = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function(key,value){
if(key=='Authsign'){
debugger;
}
return org.apply(this,arguments);
}
}
var script = document.createElement('script');
script.textContent = '(' + code + ')()';
(document.head||document.documentElement).appendChild(script);
script.parentNode.removeChild(script);
然后刷新页面然后启动该脚本。重新点击“开始生成”,可以发现会在这里断住,说明这个时候“Authsign被设置上了Header”。
然后我们开始追栈,追栈的目的也是为了找到加密参数产生的位置。
调用堆栈是当前代码的调用者,以及调用者的调用者。说明白点就是,程序执行到这里之前的所有操作,而在这里设置上了“Authsign”值,追栈就是从上往下追,最上面的是当前位置,下面的就是上一个调用位置。还不会的就只能自己去找追栈的教程看。
最后追到这个h.request这里,就和启动器进去的那个是一样的了,摁F10一步一步看,在哪里添加上了“Authsign”,就跟进去,也能定位到加密参数位置。
接着刚刚的讲,定位到加密参数之后。
通过观察可以发现这是一个webpack打包的。webpack打包的都很有特点,有个分发器。
f是,而y在上面定义了, y = n.n(f),好。
下面我们开始扣代码了,先在y=n.n(f)这里打上断点,刷新页面(模块都是一开始就加载好的所以我们是刷新页面而不是点击“开始生成”)
断住之后跳到n里面去。可以鼠标浮在n上面然后跳入该方法,也可以直接在控制台打印n,然后双击跳入该方法。
可以看到这就是webpack所谓的加载器(分发器)
我们直接复制这整个js文件, 有的教程可能会说只复制加载器就可以了,实则不然,可以发现我们的y=n.n(f)是n.n的,而n是这个o函数,就相当于o.n(f),我们在这个js文件搜索一下,可以发现下面定义了o.n,所以我们需要全部复制而不是只复制加载器。
新建js文件,粘贴到此。
定义一个全局变量lorder。把最后一个d()改成lorder=o,最一个d()是入口函数,我们用不到,这样我们就可以在外面使用lorder做o的事了。 然后我们把数组形式改为对象形式,自己建两个方法测试一下。
出异常了。window is not defined。window未定义,我们去开头定义一个var window={};
然后再运行,成功输出,说明我们的加载器没有问题。
好,接下来去复制我们需要的方法,f=n(267),y=n.n(f),也是一样,刷新页面跳进去,然后复制方法。我们可以看到n(267)并不是方法。
这个时候我们跳进n,也就是加载器那,打上断点,重新刷新页面,然后在控制台打印下标为267的函数,e就是函数数组,也就是e[267]。
跳进去之后发现里面整个js模块和其他的不一样(这个是数组形式的),而且里面又调用了一堆函数,这个时候就不管他了,一个一个复制太累人了,当然你要是喜欢一个一个复制那也可以,不过要记得对象形式前面记得改成对应的下标,也就是这样
这里就不演示了,我们使用懒人方法,一键自吐。先定位到e[267]所在js的开头,以这种开头的都是webpack打包的模块文件。
我们也复制这个js的模块,把他放我们的加载器中(注意,我们的加载器js要重新去浏览器中复制下来,得是未更改的,就是没加lorder的)。
然后需要用到渔滒大佬的ast逆向。这是大佬的项目地址。
文件 · master · 渔滒 / webpack_ast · GitCode
缺依赖补依赖,我这里在这个ast文件夹创建了一个a.js文件,然后将我们写的js(加载器以及数组模块)复制到a.js。
然后打开cmd进入到该ast文件夹,执行一下命令:
node webpack_mixer.js -l a.js -o webpack_out.js
会报错的话缺依赖补依赖,然后运行成功之后就会多出一个webpack_out.js文件,这个文件就是变成对象形式的模块。
然后将该js重新复制回我们自己的js,而且大佬还贴心的帮我们生成了全局变量供我们调用(export_function = o;),这里再次给大佬点个赞。
而代码中的n就相当于o函数,而o又赋值给了全局变量export_function,也就是说,n(267)=o(267)=export_function(267),267号方法有了,接着往下看,
我们也定义n(267),以及y,
然后是m函数,在上面定义了,我们复制过来 ,总之就是缺什么补什么
运行之后又异常了,看到这个异常就知道是缺函数了,而我们又不知道缺哪个函数,所以我们在加载器里面加上代码console.log(c),这里的c就是下标,也可以说是对象名称,这样就知道他是在哪个函数报错了,也就知道缺的是哪个函数了,
emmm,51号函数
老规矩,进到装饰器中打上断点
然后跳进51号方法并且将整个js的模块复制到加载器中,注意,我们的加载器js要重新去浏览器中复制下来,和上面那个一样,也是cmd进入到ast文件夹,执行一下命令:
node webpack_mixer.js -l a.js -o webpack_out.js
然后,将输出的js文件中的模块复制到我们自己的js加载器模块后面就可以了,
方法之间记得打逗号隔开!
放好之后保存运行,又报错了。e没有定义,
我们来找找e从哪来的。往上找可以发现e在这,是方法的参数,
然后我们看看这个方法的参数是怎么来的,往后看可以发现是.call(this, n(262), n(51)) ,也就是说,e是n(262),o是n(51)。
好,然后我们也定义一个e=n(262),保存之后再运行,又报错了,r未定义,一样的,找r在哪定义的
发现就在开头处,
我们也把r定义一下,r=n(56),保存运行报错,o未定义,o就是刚刚那个n(51),我们也定义一下,然后保存运行报错。。
在e = decodeURIComponent(l);这里报错了,URIError: URI malformed,这个就很奇怪了,看看是不是少了什么东西。好,可以看到这个m的上面确实是有一些参数的定义,我们没有,把他加上去。
好了,加上去之后依旧是保存运行报错,不过不是刚刚的错了,这次是d(...)(...)[m(...)] is not a function,看看d在哪定义,有没有做什么多余的操作。
然后可以看到,d定义之后有个操作,d.a.extend(_.a) ,而_ = n.n(m),m = n(568),
我们也加上这些定义,依旧是保存运行报错,document is not defined,
这个异常和window不一样,在上面定义也不管用,总之我们先试一下在上面定义,var document={},可以看到f是打印出来了,但是还是有问题,提示未定义属性“words”,而words又是f的属性,
总之打印然后和浏览器的对比一下吧,m("8", "fUV&")是domain,也就是域名,也就是说,document[domain]输出是undefined,
看看浏览器的,
所以说,由于我们的程序获取不到浏览器文档的域名,所以我们直接用死值就好了,改好之后保存运行,又报错了,加密模块有问题,
我们打印看看是什么加密,可以看到是AES的加密方式,
参数已经有了,我们引用nodejs的加密库,
var CryptoJS = require("crypto-js");
crypto-js自行安装,然后下面的加密也改一下,保存运行
功夫不负有心人,终于是加密成功了!放到apipost上试一下,很奇怪,没有数据,难道是加密的数据有问题?
经过不懈的努力,最后发现是
这个f的document[m("3", "jx9[")]有问题,之前说过我们程序访问不到网站的document,所以我们打印看看是需要什么值,
可以看到他也是domain域名,所以我们这个document[m("3", "jx9[")]也要替换成'tools.miku.ac',保存运行,
再次拿着加密的数据放到Apipost里,这次请求的比较久(大喜),
可以看到网站返回了base64形式的图片,终于是成功了!
我贴上核心解密代码,中间的模块就不贴了(太多了),
var CryptoJS = require("crypto-js");
var document = {}
var export_function;
!function (e) {
//这里是加载器代码
}({
//这里是模块代码
});
module.exports = export_function;
e = export_function(262)
o = export_function(51)
r = export_function(56)
f = export_function(267)
y = export_function.n(f)
h = (export_function(37), export_function(91), export_function(92), export_function(25), export_function(123), export_function(49))
d = export_function.n(h)
m = export_function(568)
_ = export_function.n(m)
d.a.extend(_.a)
var n, l, h = ["jsjiami.com.v6", "jsjSiamtirN.coEmh.Fulv6YWfbWzOB==", "bALCr8KoHEA=", "w7BQDcKBPMO7", "wqw8W8OFT1XDnw==", "TcKFbsOIw7oTwqnDrQ==", "wqM3w7c=", "w6TCqsKDw6PCsMOaRA==", "wq9Qw7rDpcKRZg==", "w7Msw5zDtsORCsOJIw==", "OlPDoQ==", "IsOIWMOowpvCtHdD"];
n = h,
l = 448,
function (e, t, o, r) {
if ((t >>= 8) < e) {
for (; --e;)
r = n.shift(),
t === e ? (t = r,
o = n.shift()) : o.replace(/[StrNEhFulYWfbWzOB=]/g, "") === t && n.push(r);
n.push(n.shift())
}
}(++l, 114688);
var m = function t(n, c) {
n = ~~"0x".concat(n);
var l = h[n];
if (void 0 === t.RVAulw) {
!function () {
var t = "undefined" != typeof window ? window : "object" === (void 0 === e ? "undefined" : Object(r.a)(e)) && "object" === (void 0 === o ? "undefined" : Object(r.a)(o)) ? o : this;
t.atob || (t.atob = function (e) {
for (var t, n, o = String(e).replace(/=+$/, ""), r = 0, c = 0, l = ""; n = o.charAt(c++); ~n && (t = r % 4 ? 64 * t + n : n,
r++ % 4) ? l += String.fromCharCode(255 & t >> (-2 * r & 6)) : 0)
n = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(n);
return l
}
)
}();
t.hRdDHj = function (e, t) {
for (var n, o = [], r = 0, c = "", l = "", h = 0, d = (e = atob(e)).length; h < d; h++)
l += "%" + ("00" + e.charCodeAt(h).toString(16)).slice(-2);
e = decodeURIComponent(l);
for (var m = 0; m < 256; m++)
o[m] = m;
for (m = 0; m < 256; m++)
r = (r + o[m] + t.charCodeAt(m % t.length)) % 256,
n = o[m],
o[m] = o[r],
o[r] = n;
m = 0,
r = 0;
for (var _ = 0; _ < e.length; _++)
r = (r + o[m = (m + 1) % 256]) % 256,
n = o[m],
o[m] = o[r],
o[r] = n,
c += String.fromCharCode(e.charCodeAt(_) ^ o[(o[m] + o[r]) % 256]);
return c
}
,
t.gjhasZ = {},
t.RVAulw = !0
}
var d = t.gjhasZ[n];
return void 0 === d ? (void 0 === t.PPSXqK && (t.PPSXqK = !0),
l = t.hRdDHj(l, c),
t.gjhasZ[n] = l) : l = d,
l
}
_ = d()()[m("0", "ZLEd")]()[m("1", "1)V^")]()
f = y.a[m("2", "ise7")]("" + _ + 'tools.miku.ac');
authsign=f + "." + CryptoJS.AES.encrypt(_, 'tools.miku.ac')[m("9", "6lDQ")]()
console.log(authsign)
本篇文章是给自己做个记录,当然也欢迎大家学习交流。