python+js逆向学习

一、简介

本文旨在介绍 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中,下方自动生成对应的接口调用信息,且支持切换生成不同语言的结果信息。

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值