Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

JS逆向-webpack打包网站实战

2022/05/29 爬虫
Word count: 4,493 | Reading time: 19min

[TOC]

Webpack逆向

webpack打包是前端js模块化压缩打包常用的手段,特征明显,比如下方的形式的代码就是webpack分发器

1
2
3
4
5
6
// 分发器
!function(x){
function xx(n){
return ..call(**.exports, ***, ***.exports, xx)
}
}()

又或者更直观的表现n[“xxx”]这种,你可以大概知道了这是调用了webpack打包的js模块代码。

webpack打包后JS依赖模块代码的固定结构

1
2
3
4
5
6
7
8
(this["webpackJsonpzsgk-pc"] = this["webpackJsonpzsgk-pc"] || []).push([[15], [function(e, t, n) {
"use strict";
e.exports = n(693)
}
// 参数固定为e, t, n
, function(e, t, n) {
e.exports = n(697)()
}

说个逆向webpack的通用方法:

  1. 先去找加密网站的加密入口。这应该是加密网站都必须要做的==> 直接根据参数名搜索参数

  2. 找到分发器的位置,或者说是加载器,n[“xxx”]这种的n就是分发器,就比如下方中的exports的位置,最后执行了d函数==>一般是runtimexxx.js中(提供环境);一般以! function(e) {的形式出现

    分发器

  3. 寻找分发编号、加密使用模块(用到了哪些模块就导入哪些模块)==>一般在chunk-lib.js,以(window.webpackJsonp = window.webpackJsonp || []).push([的形式出现

  4. 将函数入口的地方返回全局变量,最终返回: var sign; var window = global;!function(){... sign = d}, 赋值为分发器返回的d

  5. 使用自定义的sign代替webpack代码中的n进行加密

from: https://blog.csdn.net/weixin_41586984/article/details/116268341

调试技巧

定位请求参数

  1. 打开开发者工具后,F5刷新后Ctrl + Shift + F搜索参数名,如signdata,会显示多个JS文件,选择后仔细查看(点击左下角{}美观格式化按钮)。

    more: 如果文件太多,则直接通过请求的链接去找,比如user/login

  2. Network找到新发出的xhr条目后,查看Initiator里的调用栈信息,如Login;

注: 如果加密参数名称比较简单如s,比较难定位的话,可以借助请求的其他参数来查找,比如verificationCode

调试工具

  • 断点调试breakpoints
  • XHR断点: XHR/fetch breakpoints

附录-Js记录

  • 时间戳: (new Date).getTime()

  • var a = (f1(), f2(), f3())后,f1、f2、f3函数都会执行,而a最后的结果为f3的返回值

  • javascript:void(0): void 是 JavaScript 中非常重要的关键字,该操作符指定要计算一个表达式但是不返回值。

  • TypeError: window.btoa is not a function

    btoa-atob 模块没有输出一个编程接口,它只提供命令行工具。

    如果你需要转换为Base64,你可以用Buffer来完成。

    1
    console.log(Buffer.from('Hello World!').toString('base64'));

    相反的,假设你要解码的内容是一个base64编码过的字符串。

    1
    console.log(Buffer.from(b64Encoded, 'base64').toString());

做题记录

n[“str”]题型:

天安财险

var m = this.newEncrypt(JSON.stringify(h));

  • 需要对this.privaKey的值细化下,传入拿到固定的str

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //           , p = t("NFKh")          , s = t("cg2h")
    l.prototype.newEncrypt = function(l) {
    var n = p.enc.Utf8.parse(this.privaKey)
    , t = p.enc.Utf8.parse(this.privaKey)
    , e = p.enc.Utf8.parse(l)
    , a = p.AES.encrypt(e, n, {
    iv: t,
    mode: p.mode.CBC,
    padding: p.pad.Pkcs7
    });
    return p.enc.Base64.stringify(a.ciphertext)
    }

财新网

password: this.encode(this.encrypt(this.form.password)),

  • c = a("3452")n = a.n(c),看到需要依赖3452后立马Ctrl+shift+F全局搜3452,然后把整个webpack模块扒下来

中远海运

  • n(“MuMZ”)中又有r = n("XBrZ");,在另一个文件中,module需要放两个

天翼云

var t = encodeURIComponent(c["c"].Des.encrypt(this.form.email, this.form.pwd)),

webpack实现

c = (mycode("ac6a"), mycode("b3ae"))

  • 分发器和ac6a模块在同一个文件中、而ac6a模块依赖模块在另一个文件内;
  • 分发器()({}),无感叹号
  • 分发器()({})大括号中自带较多依赖模块

自己实现:直接扒下来encrypt加密的JS内容

看准网

企名片

  • u = i("x4Ab")
  • return e.encrypt_data && (e.data = Object(u.a)(e.encrypt_data)),
  • x4Ab模块依赖aqBw,aqBw又依赖YuTi、yLpj,因此依赖项中放"x4Ab"、“aqBw”、“YuTi”、"yLpj"函数定义

  • 模拟解析函数

    1
    2
    3
    function encrypt(data){
    return data && (Object(u.a)(data))
    }

n[num]题型:

大麦网

  • 删除分发器多余代码
  • var navigator = {}

掌上高考

  1. 分发器在html文件内
  2. o = (u=a(42), a.n(u)),使用到了a.n(u)即点n函数
  3. 依赖函数的给出是以数组的形式,而不是字典的形式
  4. 模块中依赖更多模块==>引入整个模块文件,但是跟"xxx"模式不同的是,由于没有用字典{"xxx": function()}的形式,因此直接require也没用TypeError: Cannot read property '42' of undefined,而是将依赖模块数组作为参数写入到分发器依赖函数中!function(e){}([...])即方括号中,从而才能找到42函数

酷我

t.data.reqId = n,

  • 直接通过n(109)定位可能不那么准确(双击后定位的函数),可以试着直接在分发器位置进行断点,然后console输出e[“109”]

  • 只要分发器定义部分(其他的删了,因为只用了l=n(109)、c=n.n(l))+依赖模块中定义109函数(整个function而不是t.exports),以及观察其中还依赖什么如n(202)、n(203)就补充拿什么

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 如果不删分发器中其他部分
    l = mycode(109)
    c = mycode.n(l) // ==>得到l
    var r = c()();
    console.log(r)

    // 由于只用到了n.n(l),所以可以删分发器大代码中其他部分, 在使用时直接让c=l
    l = mycode(109)
    c = l();
    console.log(c) // 等价于 r = l(); console(r)

文章: https://blog.csdn.net/weixin_43189702/article/details/119860838

注意:

  • require模块内容可以放在逆向JS文件里一起,而不是一定得创建新的JS文件导入

  • 先登录然后找到加密处加断点,这个断点会在发起登录请求时才触发;往上找分发器,加上断点,分发器位置的断点是在页面刷新时触发,因此要触发这个断点需要刷新页面

  • 找加密函数c["c"].Des.encrypt(this.form.email, this.form.pwd)的时候,找完整的函数如c[“c”].Des.encrypt,而不是直接找c

  • n(42), 或者n(“xxx”),可以直接搜xxx,也可以在console里面输出后找到对应的FunctionLocation来快速定位

  • 如果依赖模块是字典的形式,则分发器依赖中写字典,如!function(e){..}({ 32:function(){...}})(一般情况n(32)、n("ABCD")), 如果不是则需要传函数数组,如n(42),此处42表示的是第42个函数,见掌上高考。

  • var mycode;后赋值的位置直接在分发器的下方即可,不用在最后面

  • 提示缺少window时,定义全局变量var window = global;,(JS逆向文件、依赖文件)

    • window表示浏览器打开的窗口,在客户端JavaScript中window对象是全局的对象,所有 JavaScript 全局对象、函数以及变量均自动成为 window 对象的成员。但在nodejs中直接调用window是不存在的,而代替的是global,所以要用nodejs运行时,得用var window = global;

    • var navigator = this等价于var navigator = {},因为在NodeJS文件中运行输出this后可以发现this={},而在浏览器中this默认为window(函数或类作用域内为函数或者类实例)

    • from:JS中document和window的区别

心得

①所有webpack打包的的js都要先看懂打包后代码运行的顺序,找到加密处;②找到webpack对象,一般是 n(数字) 调用③确定分发器。④找依赖模块,有时候各包的依赖关系太多,可以直接把文件爬下来引入,如果各个包的依赖关系不多,就可以只把调用到的函数找出来放到依赖中。⑤最后剩下的就是找到你要的代码,慢慢复现调用加密/解密函数就好了。

做题案例学习视频

进阶资料

掌上高考解密过程

解析响应data.text

相应的data.text是加密的,页面通过JS解密后渲染

  1. 确定加密位置

    1
    2
    3
    4
    5
    return null != l && null !== (a = l.data) && void 0 !== a && a.text && (l.data = (n = (e = {
    iv: u.uri,
    text: l.data.text,
    SIGN: h
    }).iv,
  2. 确定分发器位置,在html内

    通过打断o = (u=a(42), a.n(u)) // 等价于 o = a(42)

  3. 确定依赖模块:给return e[a].call(c.exports, c, c.exports, r),打断点后console输出e["42"]查看a(42)位置:

1
2
3
4
5
6
7
8
function(e, t, n) {
e.exports = (e = n(21),
n(201),
n(825),
...
n(847),
e)
}

可以看到需要依赖多个,因此直接把整个文件引入

  1. 扣解密函数:注意return表达式后是逗号的情况:会从左到右执行执行,并返回最后一个。注意:JS函数并不能返回多个返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
then((function(l) {
var e, a, t, b, n;
return null != l && null !== (a = l.data) && void 0 !== a && a.text && (l.data = (n = (e = {
iv: u.uri,
text: l.data.text,
SIGN: h
}).iv,
a = e.text,
e = e.SIGN,
e = o.a.PBKDF2(e, "secret", {
keySize: 8,
iterations: 1e3,
hasher: o.a.algo.SHA256
}).toString(),
n = o.a.PBKDF2(n, "secret", {
keySize: 4,
iterations: 1e3,
hasher: o.a.algo.SHA256
}).toString(),
a = o.a.lib.CipherParams.create({
ciphertext: o.a.enc.Hex.parse(a)
}),
n = o.a.AES.decrypt(a, o.a.enc.Hex.parse(e), {
iv: o.a.enc.Hex.parse(n)
}),
// data.text解析结果
JSON.parse(n.toString(o.a.enc.Utf8)))),
v && (t = r,
b = l,
null !== (n = window.apiConfig) && void 0 !== n && null !== (n = n.filterCacheList) && void 0 !== n && n.length ? window.apiConfig.filterCacheList.forEach((function(l) {
new RegExp(l).test(t) || d.set(t, b)
})) : d.set(t, b)),
l
}

难点:

  • 跟"xxx"模式不同的是,由于没有用字典{"xxx": function()}的形式,因此直接require也没用TypeError: Cannot read property '42' of undefined,而是将依赖模块函数数组作为参数写入到分发器依赖函数中!function(e){}([...])即方括号中,从而才能找到42函数

    • 挑选push后第二个[]中的函数数组

      1
      2
      3
      4
      5
      6
      7
      (this["webpackJsonpzsgk-pc"] = this["webpackJsonpzsgk-pc"] || []).push([[15], [function(e, t, n) {
      "use strict";
      e.exports = n(693)
      },
      ...
      }
      ]]); // 第一个]
  • 理解了a.n的含义后,可以直接把o = (u=a(42), a.n(u))转化为o=a(42)

获得加密参数signsafe

大致流程跟data.text差不多,但是p = c()(g)执行时,会报错

1
2
3
4
5
6
7
8
> Md5.prototype.update = function(e) {
> if (!this.finalized) {
> var t, n = typeof e;
> if ("string" != n) {
> if ("object" != n)
> throw ERROR;
> if (null === e)
>

根据一步步调试之后发现,还是c = (u=a(291),a.n(u))直接替换出的问题

  1. Ctrl + shift + F定位参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    g = void 0,
    g = (t = {
    SIGN: h,
    str: f.replace(/^\/|https?:\/\/\/?/, "")
    }).SIGN,
    t = t.str,
    g = o.a.HmacSHA1(o.a.enc.Utf8.parse(t), g),
    g = o.a.enc.Base64.stringify(g).toString(),
    p = c()(g),
    u.signsafe = p,
  2. 往上找c和o.a: o = (u = a(42),a.n(u)), c = (u = a(291),a.n(u))

  3. 找到分发器扣出==>这次不能删除分发器中多余的函数,比如r.a、r.d、r.n因为后面得用

  4. 将依赖模块跟data.text一样,放入分发器依赖模块中

  5. 扣加密函数

    网页上是return后多段内容,以及g变量不断被修改,因此通过一步步调试确定入参,以及分解return抽离出真正的加密参数signsafe

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    h = "D23ABC@#56"
    var o = {}

    o.a = mycode(42)
    // ▲
    c = (u = mycode(291), mycode.n(u))

    /**
    * 对url进行加密
    */
    function encrypt(f) {
    g = (t = {
    SIGN: h,
    str: f.replace(/^\/|https?:\/\/\/?/, "")
    }).SIGN,
    t = t.str;
    // console.log(t, g);
    g = o.a.HmacSHA1(o.a.enc.Utf8.parse(t), g);
    // console.log(g)
    g = o.a.enc.Base64.stringify(g).toString();
    // console.log(g)
    p = c()(g);
    return p;
    }

    res = encrypt("https://api.eol.cn/web/api/counter?cid=1&did=263")
    console.log(res)

可以看到o.a和c的赋值是不一样的,虽然说大多数情况x = a.n(u)等价于x=u,但难免有时会有不一样,因此谨慎期间,还是还原到底最好。

Python调用

1
2
3
4
5
6
7
8
9
import execjs
def get_signsafe_by_javascript(url):
# 两个 JavaScript 脚本,两种方法均可
with open('gk_signsafe.js', 'r', encoding='utf-8') as f:
exec_js = f.read()
signsafe = execjs.compile(exec_js).call('encrypt', url)
return signsafe
signsafe = get_encrypted_password_by_javascript("https://api.eol.cn/web/api/counter?cid=1&did=263")
print(signsafe)

RSA的加密步骤

  1. 获取公钥
  2. 实例化 ===> 扣出网站RSA实例化对象的代码
  3. 设置公钥
  4. 对文本进行加密 ==> 扣出复现RSA加密的逻辑代码

注: var window=globalvar navigator={}

  • B站RSA

    1
    2
    3
    4
    5
    6
    7
    // window.JSEncrypt is not a constructor 在抠出来的JS Encrypt代码中加上
    window.JSEncrpt = ze
    // 网页中位var n = new JSEncrypt ==> JSEncrypt is not defined
    var n = new window.JSEncrypt;
    n.setPublicKey...
    var a = n.encrypt(t.data.hash + password);
    console.log(a)
  • 网易云爬评论:python通过execjs来调用JS代码,代码中用到了CryptoJS库, 需要os.environ["NODE_PATH"]="F:/..../node_modules"把库导入

  • JS逆向实战分析–某铁网分析:document返回类型,initiator是一条条文本(Other),因为其没有用ajax(XHR),而是通过原生的网页表单提交

    1
    2
    loginForm.password.value = encryptByDES(loginForm.password.value), loginForm.publickey.value);
    loginForm.submit();
    • 直接require导入CryptoJS模块

    • or直接扣encryptByDES的加密函数==>出现cannot read property 'createEncryptor' of undefined

    • MD5加密:

      • JS: const crypto = require("CryptoJS"); crypto.MD5('待加密字符串').toString()

      • Python: https://blog.csdn.net/weixin_44799217/article/details/112486097

        1
        2
        3
        4
        5
        6
        7
        8
        # 法一:创建md5对象
        hl = hashlib.md5()
        # Tips
        # 此处必须声明encode,若写法为hl.update(str) 报错为: Unicode-objects must be encoded before hashing
        hl.update(str.encode(encoding='utf-8'))

        # 法二:
        str_md5 = hashlib.md5(str.encode(encoding='utf-8')).hexdigest()
    • base64编码

      • JS: CryptoJS.enc.Base64.parse("待解密字符串").toString(CryptoJS.enc.Utf8)

      • Python

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        # 字符串
        encode_str = base64.encodebytes(test_str.encode('utf8')) # b'aGVsbG8gd29ybGQh\n'
        print(encode_str.decode()) # 默认以utf8解码,结果 aGVsbG8gd29ybGQh

        # 图片
        with open("D:\\redis.png", 'rb') as f:
        encode_img = base64.b64encode(f.read())
        file_ext = os.path.splitext("D:\\redis.png")[1]
        print('data:image/{};base64,{}'.format(file_ext[1:], encode_img.decode()))
        f.close()

加密、摘要算法结果特征

urlencode

urlencode是一个函数,可将字符串以URL编码,用于编码处理。

URL编码(URL encoding),也称作百分号编码(Percent-encoding), 是特定上下文的统一资源定位符 (URL)的编码机制。

Base64特征

最常见的用于传输8Bit字节码编码方式之一

  • 相同内容,结果是相同的
  • a-zA-Z,0-9,+/共64个字符进行编码;每3个字节编码成4个字节,不足的在结尾有无意义的**=**来填补
    • 一般情况下结尾都会有1个或者2个等号,明文长度是3的倍数时没有=;
  • 内容越长,结果越长

注:跟下面的算法区分一下,base64是编码方式,并不能算加密算法。应用场景还有传输图片:data:image/jpg;base64,/9j/4QMZRXhpZgAASUk...

md5特征

消息摘要算法

  • 确定唯一性:相同内容,结果是相同的;但一般会有时间戳等参数,所以导致了每次不同
  • 不可逆性:有损的加密过程,理论上无法解密(逆向推出),除非暴力破解。安全,这也是其成为校验是否被修改的最关键的性质
  • 碰撞性:原始数据与其MD5值并不是一一对应的,有可能多个原始数据计算出来的MD5值是一样的,这就是碰撞。
  • 一般MD5值是32位,由数字“0-9”和字母“a-f”所组成的字符串;字母可以是全大写或者全小写
    • 密文一般为 16 位或者 32 位,其中 16 位是取的 32 位第 9~25 位的值;
  • 长度:32个十六进制字符组成的字符串 (128位)

RSA特征

  • 相同内容,结果也是不同的
  • 明文长度需要小于密钥长度,而密文长度则等于密钥长度。一般为1024、2048、3072、4096或512(低于1024的安全不建议)
  • 通过公钥加密结果,必须私钥解密。 同样私钥加密结果,公钥可以解密

注:RSA加解密中必须考虑到的密钥长度、明文长度和密文长度问题;

▲.一般会使用 JSEncrypt 库,会有 new 一个实例对象的操作;

SHA 系列

SHA 是比 MD5 更安全一点的摘要算法,SHA 通常指 SHA 家族算法,

sha1

字母(a-f)和数字(0-9)混合

密文特征跟MD5差不多,只不过数字是40位,bit位数(160)==>4位十六进制表示一个数

Sha256

字母(a-f)和数字(0-9)混合

对于任意长度的消息,SHA256都会产生一个256位的哈希值,即64位十六进制数,称作消息摘要。

HMAC

在md5和sha1加密的基础上引入了秘钥,而秘钥又只有传输双方才知道,所以基本上是破解不了的,常用于接口签名验证

AES、DES、3DES、RC4、Rabbit 等

AES、DES、3DES、RC4、Rabbit 等加密算法的密文通常没有固定的长度,他们通常使用crypto-js库来实现

参考:https://juejin.cn/post/7052978567390429215

Author: Mrli

Link: https://nymrli.top/2022/03/23/JS逆向-webpack打包网站实战/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Python进阶
NextPost >
2022年3月19~20日-爬虫项目记录
CATALOG
  1. 1. Webpack逆向
    1. 1.1. 调试技巧
      1. 1.1.1. 定位请求参数
      2. 1.1.2. 调试工具
    2. 1.2. 附录-Js记录
  2. 2. 做题记录
    1. 2.1. n[“str”]题型:
      1. 2.1.1. 天安财险
      2. 2.1.2. 财新网
      3. 2.1.3. 中远海运
      4. 2.1.4. 天翼云
      5. 2.1.5. 看准网
      6. 2.1.6. 企名片
    2. 2.2. n[num]题型:
      1. 2.2.1. 大麦网
      2. 2.2.2. 掌上高考:
      3. 2.2.3. 酷我
    3. 2.3. 注意:
    4. 2.4. 做题案例学习视频
    5. 2.5. 进阶资料
    6. 2.6. 掌上高考解密过程
      1. 2.6.1. 解析响应data.text
      2. 2.6.2. 获得加密参数signsafe
        1. 2.6.2.1. Python调用
    7. 2.7. RSA的加密步骤
  3. 3. 加密、摘要算法结果特征
    1. 3.1. urlencode
    2. 3.2. Base64特征
    3. 3.3. md5特征
    4. 3.4. RSA特征
    5. 3.5. SHA 系列
    6. 3.6. AES、DES、3DES、RC4、Rabbit 等