关注微信公众号:K哥爬虫,QQ交流群:808574309,持续分享爬虫进阶、JS/安卓逆向等技术干货!

声明

本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!

逆向目标

  • 目标:G某游戏登录
  • 主页:aHR0cHM6Ly93d3cuZ205OS5jb20v
  • 接口:aHR0cHM6Ly9wYXNzcG9ydC5nbTk5LmNvbS9sb2dpbi9sb2dpbjM=
  • 逆向参数:

    Query String Parameters:
    password: kRtqfg41ogc8btwGlEw6nWLg8cHcCW6R8JaeM......

逆向过程

抓包分析

来到首页,随便输入一个账号密码,点击登陆,抓包定位到登录接口为 aHR0cHM6Ly9wYXNzcG9ydC5nbTk5LmNvbS9sb2dpbi9sb2dpbjM=,GET 请求,Query String Parameters 里,密码 password 被加密处理了。

加密入口

直接搜索关键字 password 会发现结果太多不好定位,使用 XHR 断点比较容易定位到加密入口,有关 XHR 断点调试可以查看 K 哥往期的教程:【JS 逆向百例】XHR 断点调试,Steam 登录逆向,如下图所示,在 home.min.js 里可以看到关键语句 a.encode(t.password, s)t.password 是明文密码,s 是时间戳。

跟进 a.encode() 函数,此函数仍然在 home.min.js 里,观察这部分代码,可以发现使用了 JSEncrypt,并且有 setPublicKey 设置公钥方法,由此可以看出应该是 RSA 加密,具体步骤是将明文密码和时间戳组合成用 | 组合,经过 RSA 加密后再进行 URL 编码得到最终结果,如下图所示:

RSA 加密找到了公钥,其实就可以直接使用 Python 的 Cryptodome 模块来实现加密过程了,代码如下所示:

import time
import base64
from urllib import parse
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5 password = "12345678"
timestamp = str(int(time.time() * 1000))
encrypted_object = timestamp + "|" + password
public_key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB"
rsa_key = RSA.import_key(base64.b64decode(public_key)) # 导入读取到的公钥
cipher = PKCS1_v1_5.new(rsa_key) # 生成对象
encrypted_password = base64.b64encode(cipher.encrypt(encrypted_object.encode(encoding="utf-8")))
encrypted_password = parse.quote(encrypted_password)
print(encrypted_password)

即便是不使用 Python,我们同样可以自己引用 JSEncrypt 模块来实现这个加密过程(该模块使用方法可参考 JSEncrypt GitHub),如下所示:

/*
引用 jsencrypt 加密模块,如果在 PyCharm 里直接使用 require 引用最新版 jsencrypt,
运行可能会提示 jsencrypt.js 里 window 未定义,直接在该文件定义 var window = this; 即可,
也可以使用和网站用的一样的 2.3.1 版本:https://npmcdn.com/jsencrypt@2.3.1/bin/jsencrypt.js
也可以将 jsencrypt.js 直接粘贴到此脚本中使用,如果提示未定义,直接在该脚本中定义即可。
*/ JSEncrypt = require("jsencrypt") function getEncryptedPassword(t, e) {
var jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB');
var i = e ? e + "|" + t : t;
return encodeURIComponent(jsEncrypt.encrypt(i));
} var password = "12345678";
var timestamp = (new Date).getTime();
console.log(getEncryptedPassword(password, timestamp));

webpack 改写

本文的标题是 webpack 改写实战,所以很显然本文的目的是为了练习 JavaScript 模块化编程 webpack 代码的改写,现在大多数站点都使用了这种写法,然而并不是所有站点都像本文遇到的站点一样,可以很容易使用其他方法来实现的,往往大多数站点需要你自己扒下他的源码来还原加密过程,有关 JavaScript 模块化编程,即 webpack,在 K 哥往期的文章中有过详细的介绍:爬虫逆向基础,理解 JavaScript 模块化编程 webpack

一个标准的 webpack 整体是一个 IIFE 立即调用函数表达式,其中有一个模块加载器,也就是调用模块的函数,该函数中一般具有 function.call() 或者 function.apply() 方法,IIFE 传递的参数是一个列表或者字典,里面是一些需要调用的模块,写法类似于:

!function (allModule) {
function useModule(whichModule) {
allModule[whichModule].call(null, "hello world!");
}
}([
function module0(param) {console.log("module0: " + param)},
function module1(param) {console.log("module1: " + param)},
function module2(param) {console.log("module2: " + param)},
]);

观察这次站点的加密代码,会发现所有加密方法都在 home.min.js 里面,在此文件开头可以看到整个是一个 IIFE 立即调用函数表达式,function e 里面有关键方法 .call(),由此可以判断该函数为模块加载器,后面传递的参数是一个字典,里面是一个个的对象方法,也就是需要调用的模块函数,这就是一个典型的 webpack 写法,如下图所示:

接下来我们通过 4 步完成对 webpack 代码的改写,将原始代码扒下来实现加密的过程。

1、找到 IIFE

IIFE 立即调用函数表达式,也称为立即执行函数,自执行函数,将源码中的 IIFE 框架抠出来,后续将有用的代码再往里面放:

!function (t) {

}({

})

2、找到模块加载器

前面我们已经讲过,带有 function.call() 或者 function.apply() 方法的就是模块加载器,也就是调用模块的方法,在本例中,function e 就是模块加载器,将其抠下来即可,其他多余的代码可以直接删除,注意里面用到了 i,所以定义 i 的语句也要抠下来:

!function (t) {
function e(s) {
if (i[s])
return i[s].exports;
var n = i[s] = {
exports: {},
id: s,
loaded: !1
};
return t[s].call(n.exports, n, n.exports, e),
n.loaded = !0,
n.exports
}
var i = {};
}({ })

3、找到调用的模块

重新来到加密的地方,第一个模块是 3,n 里面的 encode 方法最终返回的就是加密后的结果,如下图所示:

第二个模块是 4,可以看到模块 3 里面的 this.jsencrypt.encrypt(i) 方法实际上是调用的第 3340 行的方法,该方法在模块 4 里面,这里定位在模块 4 的方法,可以在浏览器开发者工具 source 页面,将鼠标光标放到该函数前面,一直往上滑动,直到模块开头,也可以使用 VS Code 等编辑器,将整个 home.min.js 代码粘贴过去,然后选择折叠所有代码,再搜索这个函数,即可快速定位在哪个模块。

确定使用了 3 和 4 模块后,将这两个模块的所有代码扣下来即可,大致代码架构如下(模块 4 具体的代码太长,已删除):

!function (t) {
function e(s) {
if (i[s])
return i[s].exports;
var n = i[s] = {
exports: {},
id: s,
loaded: !1
};
return t[s].call(n.exports, n, n.exports, e),
n.loaded = !0,
n.exports
}
var i = {};
}(
{
4: function (t, e, i) {},
3: function (t, e, i) {
var s;
s = function (t, e, s) {
function n() {
"undefined" != typeof r && (this.jsencrypt = new r.JSEncrypt,
this.jsencrypt.setPublicKey("-----BEGIN PUBLIC KEY-----略-----END PUBLIC KEY-----"))
} var r = i(4);
n.prototype.encode = function (t, e) {
var i = e ? e + "|" + t : t;
return encodeURIComponent(this.jsencrypt.encrypt(i))
},
s.exports = n
}.call(e, i, e, t),
!(void 0 !== s && (t.exports = s))
}
}
)

这里需要我们理解一个地方,那就是模块 3 的代码里有一行 var r = i(4);,这里的 i3: function (t, e, i) {},传递过来的 i,而模块 3 又是由模块加载器调用的,即 .call(n.exports, n, n.exports, e) 里面的某个参数就是 i,前面在讲解基础的时候已经说过,.call 的第一个参数指定的是函数体内 this 对象的指向,并不代表真正参数,所以第一个 n.exports 并不是参数,从第二个参数即 n 开始算,那么 i 其实就是 .call(n.exports, n, n.exports, e) 里面的 e,所以 var r = i(4); 实际上就是模块加载器 function e 调用了模块 4,由于这里模块 4 是个对象,所以这里最好写成 var r = i("4");,这里是数字,所以可以成功运行,如果模块 4 名字变成 func4 或者其他名字,那么调用时就必须要加引号了。

4、导出加密函数

目前关键的加密代码已经剥离完毕了,最后一步就是需要把加密函数导出来供我们调用了,首先定义一个全局变量,如 eFunc,然后在模块加载器后面使用语句 eFunc = e,把模块加载器导出来:

var eFunc;

!function (t) {
function e(s) {
if (i[s])
return i[s].exports;
var n = i[s] = {
exports: {},
id: s,
loaded: !1
};
return t[s].call(n.exports, n, n.exports, e),
n.loaded = !0,
n.exports
}
var i = {};
eFunc = e
}(
{
4: function (t, e, i) {},
3: function (t, e, i) {}
}
)

然后定义一个函数,传入明文密码,返回加密后的密码:

function getEncryptedPassword(password) {
var timestamp = (new Date).getTime();
var encryptFunc = eFunc("3");
var encrypt = new encryptFunc;
return encrypt.encode(password, timestamp)
}

其中 timestamp 为时间戳,因为我们最终需要调用的是模块 3 里面的 n.prototype.encode 这个方法,所以首先调用模块 3,返回的是模块 3 里面的 n 函数(可以在浏览器运行代码,一步一步查看结果),然后将其 new 出来,调用 n 的 encode 方法,返回加密后的结果。

自此,webpack 的加密代码就剥离完毕了,最后调试会发现 navigator 和 window 未定义,定义一下即可:

var navigator = {};
var window = global;

这里扩展一下,在浏览器里面 window 其实就是 global,在 nodejs 里没有 window,但是有个 global,与浏览器的 window 对象类型相似,是全局可访问的对象,因此在 nodejs 环境中可以将 window 定义为 global,如果定义为空,可能会引起其他错误。

完整代码

GitHub 关注 K 哥爬虫,持续分享爬虫相关代码!欢迎 star !https://github.com/kgepachong/

以下只演示部分关键代码,不能直接运行!完整代码仓库地址:https://github.com/kgepachong/crawler/

JavaScript 加密关键代码架构

方法一:webpack 改写源码实现 RSA 加密:

var navigator = {};
var window = global;
var eFunc; !function (t) {
function e(s) {
if (i[s])
return i[s].exports;
var n = i[s] = {
exports: {},
id: s,
loaded: !1
};
return t[s].call(n.exports, n, n.exports, e),
n.loaded = !0,
n.exports
} var i = {};
eFunc = e;
}(
{
4: function (t, e, i) {},
3: function (t, e, i) {}
}
) function getEncryptedPassword(password) {
var timestamp = (new Date).getTime();
var encryptFunc = eFunc("3");
var encrypt = new encryptFunc;
return encrypt.encode(password, timestamp)
} // 测试样例
// console.log(getEncryptedPassword("12345678"))

方法二:直接使用 JSEncrypt 模块实现 RSA 加密:

/*
引用 jsencrypt 加密模块,此脚适合在 nodejs 环境下运行。
1、使用 require 语句引用,前提是使用 npm 安装过;
2、将 jsencrypt.js 直接粘贴到此脚本中使用,同时要将结尾 exports.JSEncrypt = JSEncrypt; 改为 je = JSEncrypt 导出方法。
PS:需要定义 var navigator = {}; var window = global;,否则提示未定义。
*/ // ========================= 1、require 方式引用 =========================
// var je = require("jsencrypt") // =================== 2、直接将 jsencrypt.js 复制过来 ===================
/*! JSEncrypt v2.3.1 | https://npmcdn.com/jsencrypt@2.3.1/LICENSE.txt */
var navigator = {};
var window = global; // 这里是 jsencrypt.js 代码 function getEncryptedPassword(t) {
var jsEncrypt = new je();
jsEncrypt.setPublicKey('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB');
var e = (new Date).getTime();
var i = e ? e + "|" + t : t;
return encodeURIComponent(jsEncrypt.encrypt(i));
} // 测试样例
// console.log(getEncryptedPassword("12345678"));

Python 登录关键代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*- import re
import json
import time
import random
import base64
from urllib import parse import execjs
import requests
from PIL import Image
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5 login_url = '脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler'
verify_image_url = '脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler'
check_code_url = '脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler' headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
session = requests.session() def get_jquery():
jsonp = ''
for _ in range(21):
jsonp += str(random.randint(0, 9))
jquery = 'jQuery' + jsonp + '_'
return jquery def get_dict_from_jquery(text):
result = re.findall(r'\((.*?)\)', text)[0]
return json.loads(result) def get_encrypted_password_by_javascript(password):
# 两个 JavaScript 脚本,两种方法均可
with open('gm99_encrypt.js', 'r', encoding='utf-8') as f:
# with open('gm99_encrypt_2.js', 'r', encoding='utf-8') as f:
exec_js = f.read()
encrypted_password = execjs.compile(exec_js).call('getEncryptedPassword', password)
return encrypted_password def get_encrypted_password_by_python(password):
timestamp = str(int(time.time() * 1000))
encrypted_object = timestamp + "|" + password
public_key = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
rsa_key = RSA.import_key(base64.b64decode(public_key)) # 导入读取到的公钥
cipher = PKCS1_v1_5.new(rsa_key) # 生成对象
encrypted_password = base64.b64encode(cipher.encrypt(encrypted_object.encode(encoding="utf-8")))
encrypted_password = parse.quote(encrypted_password)
return encrypted_password def get_verify_code():
response = session.get(url=verify_image_url, headers=headers)
with open('code.png', 'wb') as f:
f.write(response.content)
image = Image.open('code.png')
image.show()
code = input('请输入图片验证码: ')
return code def check_code(code):
timestamp = str(int(time.time() * 1000))
params = {
'callback': get_jquery() + timestamp,
'ckcode': code,
'_': timestamp,
}
response = session.get(url=check_code_url, params=params, headers=headers)
result = get_dict_from_jquery(response.text)
if result['result'] == 1:
pass
else:
raise Exception('验证码输入错误!') def login(username, encrypted_password, code):
timestamp = str(int(time.time() * 1000))
params = {
'callback': get_jquery() + timestamp,
'encrypt': 1,
'uname': username,
'password': encrypted_password,
'remember': 'checked',
'ckcode': code,
'_': timestamp
}
response = session.get(url=login_url, params=params, headers=headers)
result = get_dict_from_jquery(response.text)
print(result) def main():
# 测试账号:15434947408,密码:iXqC@aJt8fi@VwV
username = input('请输入登录账号: ')
password = input('请输入登录密码: ') # 获取加密后的密码,使用 Python 或者 JavaScript 实现均可
encrypted_password = get_encrypted_password_by_javascript(password)
# encrypted_password = get_encrypted_password_by_python(password) # 获取验证码
code = get_verify_code() # 校验验证码
check_code(code) # 登录
login(username, encrypted_password, code) if __name__ == '__main__':
main()

【JS 逆向百例】webpack 改写实战,G 某游戏 RSA 加密的更多相关文章

  1. Vue-cli+Vue.js2.0+Vuex2.0+vue-router+es6+webpack+node.js脚手架搭建和Vue开发实战

    Vue.js是一个构建数据驱动的web界面的渐进式框架.在写这边文章时Vue版本分为1.0++和2.0++,这个是基于Vue2.0的项目. Vue-cli是构建单页应用的脚手架,这个可是官方的. Vu ...

  2. JS逆向实战12——某店 captchaToken 参数 加密

    今天爬取的是网站需要模拟登陆 目标网站 aHR0cHM6Ly9wYXNzcG9ydC55aGQuY29tL3Bhc3Nwb3J0L2xvZ2luX2lucHV0LmRv 浏览器抓包分析 随便输入一堆假 ...

  3. 我去!爬虫遇到JS逆向AES加密反爬,哭了

    今天准备爬取网页时,遇到『JS逆向AES加密』反爬.比如这样的: 在发送请求获取数据时,需要用到参数params和encSecKey,但是这两个参数经过JS逆向AES加密而来. 既然遇到了这个情况,那 ...

  4. 浏览器内存漫游解决方案(js逆向)

    //浏览器内存漫游解决方案(js逆向) //原理通过ast把所有的变量,参数中间值进行内存的存储 //搜索AST-hook,进入github //现在github的库下载下来 //anyproxy n ...

  5. JS逆向之浏览器补环境详解

    JS逆向之浏览器补环境详解 "补浏览器环境"是JS逆向者升职加薪的必备技能,也是工作中不可避免的操作. 为了让大家彻底搞懂 "补浏览器环境"的缘由及原理,本文将 ...

  6. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  7. Java使用正则表达式取网页中的一段内容(以取Js方法为例)

    关于正则表达式: 表1.常用的元字符 代码 说明 . 匹配除换行符以外的任意字符 \w 匹配字母或数字或下划线或汉字 \s 匹配任意的空白符 \d 匹配数字 \b 匹配单词的开始或结束 ^ 匹配字符串 ...

  8. UML建模语言入门 -- 用例视图详解 用例视图建模实战

    . 作者 :万境绝尘  转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/18964835 . 一. 用例视图概述 用例视图表述哪些 ...

  9. 【UML 建模】UML建模语言入门 -- 用例视图详解 用例视图建模实战

    . 作者 :万境绝尘  转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/18964835 . 一. 用例视图概述 用例视图表述哪些 ...

  10. 【算法】C语言趣味程序设计编程百例精解

    C语言趣味程序设计编程百例精解 C/C++语言经典.实用.趣味程序设计编程百例精解(1)  https://wenku.baidu.com/view/b9f683c08bd63186bcebbc3c. ...

随机推荐

  1. Spark 覆盖写Hive分区表,只覆盖部分对应分区

    要求Spark版本2.3以上,亲测2.2无效 配置 config("spark.sql.sources.partitionOverwriteMode","dynamic& ...

  2. 学习 Java 还是 Solon 简单,v1.9.0 发布

    入手很简单哦: pom.xml 添加依赖 <dependency> <groupId>org.noear</groupId> <artifactId>s ...

  3. python jira 读取表数据批量新建子任务

    小李在Jira中处理任务时,发现一个表格数据很有趣.他决定为每一行数据创建一个新的子任务.他复制粘贴,忙得不亦乐乎.同事小张路过,好奇地问:"你在做什么?"小李得意地回答:&quo ...

  4. BBS项目(一): 表设计 注册功能 登录功能 生成随机验证码

    目录 表设计 1.确定表的数量 2.确定表的基础字段 自关联字段 3.确定表的外键字段 表关系图 项目初建流程备忘 注册功能 登录功能 生成随机验证码 表设计 # 仿造博客园项目 核心:文章的增删改查 ...

  5. 原创内容屡屡被盗?从源头对资源盗用说NO

    在这个信息化的时代,资源被盗用是一件很让人厌恶,但又很常见的事.比如,之前郭敬明的小说<梦里花落知多少>剽窃庄羽小说<圈里圈外>一事,虽然郭敬明通过个人微博向庄羽道歉,并表示& ...

  6. 【Boost】Windows 下个人在配置 Boost 踩到的坑以及解决方案

    要编译的 Boost 版本:1.82.0 假设 Boost 的根目录为 ${boost_root}$ 如果不想编译可以使用博主编译后的Boost:https://pan.baidu.com/s/1s2 ...

  7. L3-013 非常弹的球 (30 分) (math)

    刚上高一的森森为了学好物理,买了一个"非常弹"的球.虽然说是非常弹的球,其实也就是一般的弹力球而已.森森玩了一会儿弹力球后突然想到,假如他在地上用力弹球,球最远能弹到多远去呢?他不 ...

  8. vivo 全球商城:从 0 到 1 代销业务的融合之路

    代销是 vivo 商城已经落地的成熟业务,本文提供给各位读者 vivo 商城代销业务中两个异构系统业务融合的对接经验和架构思路. 一.业务背景 近两年,内销商城业务的发展十分迅速,vivo 商城系统的 ...

  9. 活动回顾|阿里云云原生 Serverless 技术实践营 深圳站回放&PPT下载

    11月24日"阿里云云原生 Serverless 技术实践营"深圳站圆满落幕.活动受众以关注  Serverless 技术的开发者.企业决策人.云原生领域创业者为主,活动形式为演讲 ...

  10. 十三、docker的四种网络类型

    系列导航 一.docker入门(概念) 二.docker的安装和镜像管理 三.docker容器的常用命令 四.容器的网络访问 五.容器端口转发 六.docker数据卷 七.手动制作docker镜像 八 ...