一、简介
本文旨在介绍 JavaScript 逆向工程、调试技术以及处理兼容性问题的基本原理和实践方法。通过深入理解 JavaScript 的内部机制和常见的调试技术,读者将能够更好地解决 JavaScript 应用程序中的问题,并扩展对 JavaScript 的学习和研究
需要注意的是,在进行 JavaScript 逆向工程时,需要遵守相关法律和道德规范,并尊重原作者的知识产权。逆向工程应仅用于合法目的,例如调试、逆向工程兼容性问题、学习和研究等。
在 Python 中,可以使用 requests 库来发送 HTTP 请求,并使用 execjs 库来执行 JavaScript 代码。这种组合可以用于执行 JavaScript 逆向工程。
二、方案实现
1、寻找目标接口(以百度搜索为例)
首先,发送请求后,根据接口返回信息是否包含需要的内容判断是否为目标接口
2、观察接口请求的内容信息
发现query值、sign值与ts都会发生变化,query的值好说只是我们需要翻译的单词,然而这个sign的值却是不确定的,而且相同的query的sign值是相同的。ts每次的不一样,推测为时间戳。
3、搜索关键词,并进行断点调试以及参数分析
切换到“source”查看sign有什么规律或者说看他是怎么被生成的,我们全局搜索一下sign(ctrl+sheft+f),查看哪个文件中含有“sign:”关键字,通过观察,发现index.5af2d87e.js为目标文件。
打开目标文件,在文件中搜索“sign:”,发现只有六个结果,我们对所有的sign内容打上断点,点击“立即翻译”继续调这个接口,进入断点调试。
在输入框中输入内容,进行翻译操作,不断恢复脚本执行,直到断点标记处出现了“sign”关键字,发现变量w包含了表单的所有内容,判断次数多半是我们要寻找的结果。
其中,变量e则是需要翻译的内容,sign的值则是将e赋值给方法b()得到的,而根据其中内容查看,ts确定就是时间戳。
import time
#由于python中的时间为秒,因此需要乘以1000后才能得到与在js代码中相同的结果
ts = str(int(time.time())*1000)
print(ts)
4、寻找目标函数
将鼠标悬浮至b(e)方法的上方,会出现该方法本体的链接信息。
点击找到的链接地址,就可以跳转至b(e)方法本体的位置,光标定位在t.exports = function(t)上,未知该函数中的t变量是什么内容,在此处打个断点,观察发现其中的t就是需要翻译的内容也就是b(e)中的e,而该函数则是我们要找的目标函数了。
将该函数的js代码复制保存到一个新建的js文件中,随便给该方法增加一个名字,如:test(t)。
复制并适当调整到的函数如下:
function test(t){
var o, i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === i) {
var a = t.length;
a > 30 && (t = "".concat(t.substr(0, 10)).concat(t.substr(Math.floor(a / 2) - 5, 10)).concat(t.substr(-10, 10)))
} else {
for (var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), c = 0, u = s.length, l = []; c < u; c++)
"" !== s[c] && l.push.apply(l, function(t) {
if (Array.isArray(t))
return e(t)
}(o = s[c].split("")) || function(t) {
if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"])
return Array.from(t)
}(o) || function(t, n) {
if (t) {
if ("string" == typeof t)
return e(t, n);
var r = Object.prototype.toString.call(t).slice(8, -1);
return "Object" === r && t.constructor && (r = t.constructor.name),
"Map" === r || "Set" === r ? Array.from(t) : "Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) ? e(t, n) : void 0
}
}(o) || function() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")
}()),
c !== u - 1 && l.push(i[c]);
var p = l.length;
p > 30 && (t = l.slice(0, 10).join("") + l.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") + l.slice(-10).join(""))
}
for (var d = "".concat(String.fromCharCode(103)).concat(String.fromCharCode(116)).concat(String.fromCharCode(107)),
h = (null !== r ? r : (r = window[d] || "") || "").split("."), f = Number(h[0]) || 0, m = Number(h[1]) || 0, g = [], y = 0, v = 0; v < t.length; v++) {
var _ = t.charCodeAt(v);
_ < 128 ? g[y++] = _ : (_ < 2048 ? g[y++] = _ >> 6 | 192 : (55296 == (64512 & _) && v + 1 < t.length && 56320 == (64512 & t.charCodeAt(v + 1)) ? (_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v)),
g[y++] = _ >> 18 | 240,
g[y++] = _ >> 12 & 63 | 128) : g[y++] = _ >> 12 | 224,
g[y++] = _ >> 6 & 63 | 128),
g[y++] = 63 & _ | 128)
}
for (var b = f, w = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(97)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(54)),
k = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(51)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(98)) + "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(102)),
x = 0; x < g.length; x++)
b = n(b += g[x], w);
return b = n(b, k),
(b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
"".concat((b %= 1e6).toString(), ".").concat(b ^ f)
}
5、调用函数,并逐步补全js代码
接下来会在 python 用到 execjs 这个库执行 JS 代码(前提需要需要确保你的系统上安装了Node.js或其他JavaScript运行环境才能运行js代码),所以可以先写一个 demo 测试,调用这个 JS 代码的函数,如果出现报错,则会直接在控制台显示。
import execjs,os
query = input('请输入需要翻译的内容:')
with open(os.getcwd() + '\\js_reverse_baidu_fanyi_index.js', 'r', encoding='utf-8')as f:
read_file = f.read()
# print(read_file)
# 使用execjs类的compile()方法编译加载上面的打开的文件,返回一个上下文对象
res = execjs.compile(read_file)
# 调用JavaScript函数,并传入对应的参数,其中“b”为js代码的方法名
sign = res.call('test', query)
print(sign)
此时,控制台报错,提示这个函数中的r没有定义,这时候我们返回网页看一下这个r是个什么内容?
发现这个r在每次进行翻译时都是固定值,所以直接在js文件中定义这个变量即可
var r = "320305.131321201"
继续运行该js文件
发现它又提示n没有定义,又返回网页看一下这个方法中的n的内容
点击链接,跳转至n方法的本体位置,同理,将该方法复制到js文件中,然后继续运行该js文件。
function n(t, e) {
for (var n = 0; n < e.length - 2; n += 3) {
var r = e.charAt(n + 2);
r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r),
r = "+" === e.charAt(n + 1) ? t >>> r : t << r,
t = "+" === e.charAt(n) ? t + r & 4294967295 : t ^ r
}
return t
}
此时已经可以正常运行了,可以正常得到sign的结果,用同样的内容在网页搜索时,得到的sign也一致,所以该内容也就破解成功了。
6、完整js代码
function n(t, e) {
for (var n = 0; n < e.length - 2; n += 3) {
var r = e.charAt(n + 2);
r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r),
r = "+" === e.charAt(n + 1) ? t >>> r : t << r,
t = "+" === e.charAt(n) ? t + r & 4294967295 : t ^ r
}
return t
}
//var r = "320305.131321201"
/*
gtk为固定值,可直接定义为变量“r”,此时方法function b(t)无需接收r参数;
也可由参数传入,此时需要接收gtk内容才能得到sign内容,下方代码经过修改,增加r参数接收
*/
function test(t,r){
var o, i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === i) {
var a = t.length;
a > 30 && (t = "".concat(t.substr(0, 10)).concat(t.substr(Math.floor(a / 2) - 5, 10)).concat(t.substr(-10, 10)))
} else {
for (var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), c = 0, u = s.length, l = []; c < u; c++)
"" !== s[c] && l.push.apply(l, function(t) {
if (Array.isArray(t))
return e(t)
}(o = s[c].split("")) || function(t) {
if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"])
return Array.from(t)
}(o) || function(t, n) {
if (t) {
if ("string" == typeof t)
return e(t, n);
var r = Object.prototype.toString.call(t).slice(8, -1);
return "Object" === r && t.constructor && (r = t.constructor.name),
"Map" === r || "Set" === r ? Array.from(t) : "Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) ? e(t, n) : void 0
}
}(o) || function() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")
}()),
c !== u - 1 && l.push(i[c]);
var p = l.length;
p > 30 && (t = l.slice(0, 10).join("") + l.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") + l.slice(-10).join(""))
}
for (var d = "".concat(String.fromCharCode(103)).concat(String.fromCharCode(116)).concat(String.fromCharCode(107)),
h = (null !== r ? r : (r = window[d] || "") || "").split("."), f = Number(h[0]) || 0, m = Number(h[1]) || 0, g = [], y = 0, v = 0; v < t.length; v++) {
var _ = t.charCodeAt(v);
_ < 128 ? g[y++] = _ : (_ < 2048 ? g[y++] = _ >> 6 | 192 : (55296 == (64512 & _) && v + 1 < t.length && 56320 == (64512 & t.charCodeAt(v + 1)) ? (_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v)),
g[y++] = _ >> 18 | 240,
g[y++] = _ >> 12 & 63 | 128) : g[y++] = _ >> 12 | 224,
g[y++] = _ >> 6 & 63 | 128),
g[y++] = 63 & _ | 128)
}
for (var b = f, w = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(97)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(54)),
k = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(51)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(98)) + "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(102)),
x = 0; x < g.length; x++)
b = n(b += g[x], w);
return b = n(b, k),
(b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
"".concat((b %= 1e6).toString(), ".").concat(b ^ f)
}
三、接口实现
1、获取 gtk, token, session 等信息
打开网页,发现token值为window.common.token 表示该内容是一个可以在浏览器环境中的全局可访问的变量信息,因此,token信息可以在网页中获取到,因此可以访问后可在页面中读取该内容。
通过r = "320305.131321201",在网页中搜索得到r即网页中的gtk
# -*- coding: utf-8 -*-
import requests,re
token_url = 'https://fanyi.baidu.com/'
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'max-age=0',
'Connection': 'keep-alive',
'Host': 'fanyi.baidu.com',
'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="8"',
'sec-ch-ua-mobile': '?0',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
}
# 使用requests.session()实例化一个会话对象
# 要先请求一次百度翻译才能拿到cookie保存并cookie。
cookie = requests.session()
#再次调用接口,获取html页面中的token值与gtk
cookie.get(token_url, headers=headers)
html = cookie.get(token_url, headers=headers).text
#通过re匹配文件中的token信息与gtk信息(gtk可用于后续的sign获取)
#其中使用search(?P<分组名>需要匹配的),可以获得带别名的分组
token = re.search(r"token: '(?P<token>.*?)',", html).group('token')
gtk = re.search(r'window.gtk = "(?P<gtk>.*?)";', html).group('gtk')
print(f'获取到的token为:{token}\n获取到的gtk为:{gtk}\n获取到的cookie信息:{cookie}', end='\n\n')
2、判断翻译的语言类型
通过在进行翻译前调用的/langdetect接口发现,该接口为判断翻译内容的语言类型的
#其中的cookie为沿用上面的获取token值中实例化的会话对象requests.session()
langdetect = 'https://fanyi.baidu.com/langdetect'
form_data = {
'query': query,
}
lan_resp = cookie.post(url=langdetect, data=form_data).json()
print(f'语言识别接口返回值:{lan_resp}')
lan_type = lan_resp['lan']
if lan_type == 'zh':
print(f'自动识别的语言为:“中文-{lan_type}”')
elif lan_type == 'en':
print(f'自动识别的语言为:“英文-{lan_type}”')
else:
print(f'自动识别的语言为:“{lan_type}”')
3、实现翻译
def translation(query, sign, token, cookie, lan_type):
'''
该方法为翻译的实现,调用翻译接口进行内容的翻译,
根据get_lan()方法返回的的语言类型判断如何翻译
:param query:
:param sign:
:param token:
:param session:
:param lan_type:
:return:
'''
if lan_type != 'zh':
toLanguge = 'zh'
else:
toLanguge = 'en'
fanyi_api = f'https://fanyi.baidu.com/v2transapi'
form_data = {
'from': lan_type,
'to': toLanguge,
'query': query,
'transtype':'realtime',
'simple_means_flag': '3',
'sign': sign,
'token': token,
'domain': 'common',
'ts':ts
}
result = cookie.post(url=fanyi_api, headers=headers, data=form_data)
response_header = result.headers
response_url = result.url
response_cookie = result.cookies
print(f'{response_url}\n{response_header}\n{response_cookie}')
res = result.json()
print(f'接口返回值:{res}')
source_languge = res['trans_result']['data'][0]['src']
end_to_languge = res['trans_result']['data'][0]['dst']
print(f'{"-"* 10}百度翻译结果{"-"* 10}')
print(f'翻译前:{query},返回数据的翻译内容:{source_languge}')
print('翻译后:', end_to_languge)
4、完整代码
'''
由于百度翻译时,会将用户的翻译内容签名认证,若不进行sign破解,则无法按照输入的内容进行翻译,
该篇内容为使用js逆向对百度翻译的sign进行破解,从而实现百度翻译的接口调用
'''
import requests,time,os,execjs,re
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'max-age=0',
'Connection': 'keep-alive',
'Host': 'fanyi.baidu.com',
'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="8"',
'sec-ch-ua-mobile': '?0',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
}
#获取当前时间戳,用于百度翻译时的ts参数
ts = str(int(time.time())*1000)
def getData():
'''
通过访问百度翻译网页,就可以获取 gtk, token, session 等信息
:return:
'''
token_url = 'https://fanyi.baidu.com/'
# 使用requests.session()实例化一个会话对象
# 要先请求一次百度翻译才能拿到cookie保存并cookie。
cookie = requests.session()
#再次调用接口,获取html页面中的token值与gtk
cookie.get(token_url, headers=headers)
html = cookie.get(token_url, headers=headers).text
#通过re匹配文件中的token信息与gtk信息(gtk可用于后续的sign获取)
#其中使用search(?P<分组名>需要匹配的),可以获得带别名的分组
token = re.search(r"token: '(?P<token>.*?)',", html).group('token')
gtk = re.search(r'window.gtk = "(?P<gtk>.*?)";', html).group('gtk')
print(f'获取到的token为:{token}\n获取到的gtk为:{gtk}\n获取到的cookie信息:{cookie}', end='\n\n')
return gtk, token, cookie
def getLan(cookie, query):
'''
# 获取自动识别输入内容的语言类型
:param session:
:param query:
:return:
'''
langdetect = 'https://fanyi.baidu.com/langdetect'
form_data = {
'query': query,
}
lan_resp = cookie.post(url=langdetect, data=form_data).json()
print(f'语言识别接口返回值:{lan_resp}')
lan_type = lan_resp['lan']
if lan_type == 'zh':
print(f'自动识别的语言为:“中文-{lan_type}”')
elif lan_type == 'en':
print(f'自动识别的语言为:“英文-{lan_type}”')
else:
print(f'自动识别的语言为:“{lan_type}”')
return lan_type
def getSign(Query,gtk):
'''
使用execjs执行js代码,从而获取到查询内容的sign
其参数中的gtk为固定值,可直接定义为变量“r”,此时方法index.js的function b(t)无需接收r参数;
也可由参数传入,此时需要接收gtk内容才能得到sign内容
:param Query:
:param gtk:
:return:
'''
with open(os.getcwd()+'\\js_reverse_baidu_fanyi_index.js', 'r', encoding='utf-8')as f:
read_file = f.read()
# print(read_file)
#使用execjs类的compile()方法编译加载上面的打开的文件,返回一个上下文对象
res = execjs.compile(read_file)
#调用JavaScript函数,并传入对应的参数,其中“b”为js代码的方法名
sign = res.call('test', Query,gtk)
return sign
def translation(query, sign, token, cookie, lan_type):
'''
该方法为翻译的实现,调用翻译接口进行内容的翻译,
根据get_lan()方法返回的的语言类型判断如何翻译
:param query:
:param sign:
:param token:
:param session:
:param lan_type:
:return:
'''
if lan_type != 'zh':
toLanguge = 'zh'
else:
toLanguge = 'en'
fanyi_api = f'https://fanyi.baidu.com/v2transapi'
form_data = {
'from': lan_type,
'to': toLanguge,
'query': query,
'transtype':'realtime',
'simple_means_flag': '3',
'sign': sign,
'token': token,
'domain': 'common',
'ts':ts
}
result = cookie.post(url=fanyi_api, headers=headers, data=form_data)
response_header = result.headers
response_url = result.url
response_cookie = result.cookies
print(f'{response_url}\n{response_header}\n{response_cookie}')
res = result.json()
print(f'接口返回值:{res}')
source_languge = res['trans_result']['data'][0]['src']
end_to_languge = res['trans_result']['data'][0]['dst']
print(f'{"-"* 10}百度翻译结果{"-"* 10}')
print(f'翻译前:{query},返回数据的翻译内容:{source_languge}')
print('翻译后:', end_to_languge)
if __name__ == '__main__':
query = input('请输入需要翻译的内容:')
gtk, token, cookie = getData()
sign = getSign(query,gtk)
lan_type = getLan(cookie, query)
translation(query, sign, token, cookie, lan_type)
四、curlconverter工具的使用(附加)
由于每次进行接口调用时,接口的headers、data等信息需要手动变成正确格式的json内容,比较浪费时间,因此,可以使用curlconverter工具进行快捷生成上述信息。
1、右键需要调用的接口,选择copey as curl(bash)
2、打开curlconverter,将链接粘贴到curl command中,下方自动生成对应的接口调用信息,且支持切换生成不同语言的结果信息。