总的来说,JavaScript 逆向可以分为三大部分:寻找入口、调试分析和模拟执行。下面分别进行介绍。
一,寻找入口
一个网站加载了很多 JavaScript 文件,那么怎么从这么多 JavaScript 里面找到关键的位置,那就是一个关键问题。这就是寻找入口。
但开始寻找前,建议先做以下工作:
-
判断参数是否需要逆向,可不可以直接复制参数
-
加密参数是否由之前的请求返回
-
按加密字符串的特征进行尝试,也许会有意外收获
下面介绍寻找入口时常用的手段。
1.1 巧用搜索
搜索是有一定技巧的,比如加密关键词是 signature,可以搜索 signature、“signature”、signature=、signature: 、sign
还可以搜索URL中的某些关键字、headers[ 等。
若遇到请求参数或者响应加密的情况,但通过跟栈找不到明文,有可能加密是在拦截器中处理的(请求拦截器Vt.interceptors.request、响应拦截器Vt.interceptors.response),因此可以搜索interceptors关键字。
PS:
在已有的页面链接中寻找其他页面的连接(从银行的详情页中搜索列表页链接):
- 搜索关键字寻找
- 搜索现有的url的某一部分关键字,有很大可能这些关键字会放在一起
1.2 Ajax断点+调用栈
它可以在发生 Ajax 请求的时候触发断点。要设置断点,就要先观察 Ajax 请求,假设请求的 URL 为:https://spa2.scrape.center/api/movie/?limit=xxx&page=99
可以看到,URL 中包含 /api/movie ,因此可以填写 /api/movie ,当 Ajax 请求的 URL 包含填写的内容时,就会进入断点停止。
1.3 Initiator call stack
Initiator 主要是为了监听请求是怎样发起的,通过它可以快速定位到调用栈中。栈的调用顺序为从下到上。
1.4 事件监听+调用栈
有的时候找不到参数位置,但是知道它的触发条件,此时可以使用事件监听器进行断点。然后结合调用栈进行定位。
1.5 Hook 关键方法
Hook 技术又叫钩子技术,指在程序运行的过程中,对其中的某个方法进行重写,在原先的方法前后加入我们自定义的代码。相当于在系统没有调用该函数之前,钩子程序就先捕获该消息,得到控制权,这时钩子函数既可以加工处理该函数的执行行为,也可以强制结束消息的传递。
要对 JavaScript 代码进行 Hook 操作,就需要额外在页面中执行一些有关 Hook 逻辑的自定义代码,这里推荐一个浏览器插件,叫做 Tampermonkey,中文叫 油猴,利用它,可以在浏览器加载页面时自动执行某些 JavaScript 脚本。由于执行的是 JavaScript ,所以我们几乎可以在网页中完成任何我们想实现的效果,如自动爬虫、自动修改页面、自动响应事件等。
下面以 https://login1.scrape.center/ 网址为例,进行 base64 方法的 hook,以下为油猴中的脚本:
// ==UserScript==
// @name HookBase64
// @namespace https://login1.scrape.center/
// @version 0.1
// @description Hook Base64 encode function
// @author Vahan
// @match https://login1.scrape.center/
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant none
// ==/UserScript==
(function() {
'use strict';
function hook(object, attr){
var func = object[attr]
object[attr] = function(){
console.log('hooked',object,attr)
var ret = func.apply(object,arguments)
debugger
return ret
}
}
hook(window,'btoa')
})();
如果观察出一些门道,可以多使用这种方法来尝试,如 Hook encode 方法,decode 方法,stringify 方法,log 方法,alert 方法等。
还可以利用浏览器中的snippet来进行hook, 当创建好一个 Snippet 片段后,可以从任何页面访问和运行它们(浏览器会一直保存创建的snippet)。当你运行一个片段,它从当前打开页面的上下文中执行(即使这个页面中的链接变化也依然生效)。
如何创建一个 Snippet 片段?
先打开开发者工具,进入到 Sources (源代码)面板,在导航栏里面选中 Snippets(代码段),点击 “New snippet(新建一个代码块)” ,输入你的代码,按Ctrl+S保存,run运行。
这两种方式都可以进行hook,油猴在应用方面的扩展性更强,但如果只是Hook调试用,snippet会更方便。
下面介绍利用snippet定位时的常用Hook脚本:
1,定位header关键字:
(function (){
var org = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function (key,value){
//在请求中发现某个关键字,断点
if(key=='value'){
debugger
}
return org.apply(this,arguments)
}
})()
这段代码的目的是在设置包含特定键的HTTP请求头时,自动触发一个断点。这可以用于调试或监视特定的网络请求头设置。
2,定位url关键字:
(function () {
var open = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function (method, url, async) {
if (url.indexOf("key") != -1) {
debugger;
}
return open.apply(this, arguments);
};
})();
这段代码的目的是在发送包含特定字符串(在这个例子中是"key")的HTTP请求的URL时,自动触发一个断点。这可以用于调试或监视特定的网络请求。
3,定位cookie关键字:
(function() {
"use strict";
var cookieTemp = "";
Object.defineProperty(document, "cookie", {
set: function(val) {
if (val.indexOf("cookie的参数名称") != -1) {
debugger ;
}
cookieTemp = val;
return val;
},
get: function() {
return cookieTemp;
}
})
}
)();
Object.defineProperty(document, “cookie”, { … }) 这行代码使用Object.defineProperty方法修改了document对象上cookie属性的行为。现在,每当尝试获取或设置document.cookie的值时,都会分别调用定义的getter和setter函数。
这段代码演示了如何使用JavaScript的Object.defineProperty方法来拦截和自定义对document.cookie属性的访问和修改行为。
4,定位JSON.stringify、JSON.parse:
'''请求是密文'''
(function() {
var _stringify = JSON.stringify;
JSON.stringify = function(ps) {
console.log("Hook JSON.stringify ——> ", ps);
debugger;
return _stringify(ps); // 不改变原有的执行逻辑
}
})();
'''响应是密文'''
(function() {
var _parse = JSON.parse;
JSON.parse = function(ps) {
console.log("Hook JSON.parse ——> ", ps);
debugger;
return _parse(ps); // 不改变原有的执行逻辑
}
})();
一般用于请求和响应是密文。当请求加密时,会先将对象进行JSON.stringify处理,使对象变成json字符串格式,然后进行加密处理;当响应解密时,会对解密后的字符串进行JSON.parse处理,使json字符串变成对象,方便后续调用。
若想从案例中练习这些hook技巧,这里推荐大家阅读:https://blog.csdn.net/randy521520/article/details/134091541
二,调试分析
找到入口之后,我们需要搞清楚里面的逻辑是怎样的、里面调用了多少加密算法,经过了多少变量赋值和转换等,以便后面进行模拟调用或者逻辑改写。在这个过程中,我们主要借助浏览器的调试工具进行断点调试分析,或者借助于一些反混淆工具进行代码的反混淆等。
以下总结了一些调试分析时有用的技巧:
扣代码:扣代码时,同级目录的代码要扣全;
调试按钮:用的最多是逐行调试:
调用栈:可以看到 $_CCBk 的上一步是 $_CGII ,再上一步是 (anonymous)
在浏览器断点调试时,当你看到“call stack”中包含promise.then,这通常意味着当前的执行上下文是在处理一个Promise对象的.then()方法中的回调函数。
Promise是JavaScript中用于异步操作的对象,可能是一个网络请求、文件读取、定时器等。
.then()方法是Promise对象上的一个方法,用于注册当Promise状态变为fulfilled时要调用的回调函数,以及可选的当Promise状态变为rejected时要调用的回调函数。
这个方法返回一个新的Promise实例,这个新的Promise实例的状态由回调函数的执行结果决定。
改写 JavaScript 文件:
我们知道,一个网页里面的 JavaScript 是从对应服务器上下载下来并在浏览器执行的。有时,可能想要在调试的过程中对 JavaScript 做一些更改,比如说有以下需求:
- 发现 JavaScript 文件中包含很多阻扰调试的代码或者无效代码、干扰代码,想要将其删除
- 调试到某处,想要加一行 console.log 输出一些内容,以便观察某个变量或方法在页面加载过程中的调用情况。在某些情况下,这种方法比打断点调试更方便
- 调试过程遇到某个局部变量或方法,想要把它赋值给 window 对象以便全局可以访问或调用
- 在调试的时候,得到的某个变量中可能包含一些关键的结果,想要加一些逻辑将这些结果转发到对应的目标服务器
浏览器中的 Sources 面板的 Overrides 功能可以支持实现。在 Overrides 上选定一个本地的文件夹,用于保存需要更改的 JavaScript 文件。更改后保存,可以发现在选定的文件夹下生成了新的 JavaScript 文件,它用于在浏览器加载该 JavaScript 文件时进行替换。
注意:若使用其他网站调试时生成的本地文件夹,则对于这个网页来说可能不能成功保存 JavaScript 文件,需要新创建一个文件夹。
三,模拟执行
搞清楚整个逻辑后,就需要对整个加密过程进行逻辑复写或者模拟执行,以把整个加密流程模拟出来。
3.1 Python 改写或模拟执行
如果整体逻辑不复杂的话,可以尝试用 python 把整个加密流程实现一遍。
如果整体逻辑较复杂,可以使用 python 来模拟调用 JavaScript 的执行。这种方法需要抠出加密代码及需要的运行环境(可能需要补浏览器环境)。
# pip install PyExecJS
# 前提是已经安装nodejs,相当于通过python来调用nodejs执行js文件
import execjs
#1.实例化一个node对象
node = execjs.get()
#2.js源文件编译
ctx = node.compile(open('./wechat.js',encoding='utf-8').read())
#3.执行js函数,wechat.js 文件中有 getpwd() 函数
funcName = 'getpwd("{}")'.format('123456')
pwd = ctx.eval(funcName)
print(pwd)
3.2 Nodejs 模拟执行 + API
与 python 调用几乎一样,都是将算法代码与调用代码抠出来放在一个 js 文件中,但 nodejs 调用的兼容性更好,需要修改调整的地方较 python 调用少,只是为了让 python 代码能使用,还需要通过 express 暴露成 HTTP 服务,从而实现跨语言的调用。
有了方式一,这种方式作用不大。
3.3 浏览器模拟执行
在算法难以实现时,可以使用 playwright 改写 js 文件,然后进行调用。这种方法只需定位到算法的位置,然后将加密函数挂到全局 window ,当作黑盒调用即可。
安装 playwright :
pip3 install playwright
playwright install
案例:https://spa2.scrape.center/
想要获取电影数据,就需要分析出 token 的加密逻辑:
使用 XHR 断点,定位到核心加密算法的位置如下:
将修改后的整个 js 文件保存到本地,命名为 chunk.js:
python 调用代码如下:
from playwright.sync_api import sync_playwright
import time
import requests
BASE_URL = 'https://spa2.scrape.center/'
INDEX_URL = BASE_URL + '/api/movie?limit={limit}&offset={offset}&token={token}'
MAX_PAGE = 10
LIMIT = 10
context = sync_playwright().start()
browser = context.chromium.launch(headless=True)
page = browser.new_page()
page.route( # 指定路由
"**/js/chunk-10192a00.243cb8b7.js",
lambda route: route.fulfill(path="./chunk.js")
)
page.goto(BASE_URL)
def get_token(offset):
result = page.evaluate('''() => {
return window.encrypt("%s","%s")
}''' % ('/api/movie', offset))
return result
for i in range(MAX_PAGE):
offset = i * LIMIT
token = get_token(offset)
index_url = INDEX_URL.format(limit=LIMIT,offset=offset,token=token)
response = requests.get(index_url)
print('response',response.json())
BASE_URL的值来自下图位置:
这种方法似乎是一种神技,以后实操用起来。
emm,在对今日头条案例的_signature使用这种方法时,未成功,返回None。暂时没有找到是哪里出了问题。
下面这种形式更加通用:
from playwright.sync_api import sync_playwright
BASE_URL = 'https://static.geetest.com/static/js/slide.7.9.2.js'
context = sync_playwright().start()
browser = context.chromium.launch(headless=True)
page = browser.new_page()
page.route(
"**/static/js/slide.7.9.2.js",
lambda route: route.fulfill(path="chunk.js")
)
page.goto(BASE_URL)
result = page.evaluate('''() => {
return window.vahan;
}''')
print(result)