文章

逆向js

逆向js

识别算法类型

代码特征算法测试记录要写
CryptoJS.AES, key+ivAES模式 ECB/CBC,密钥、IV
JSEncrypt, publicKeyRSA公钥,填充方式
CryptoJS.HmacSHA256HMAC-SHA256签名密钥
CryptoJS.MD5 / SHA256Hash 摘要有无加盐 salt

关键词清单

第一层:直球关键词

1
encrypt  decrypt  AES  DES  RSA  sign  signatureCryptoJS  crypto-js  JSEncrypt  sm-crypto  sm4

第二层:参数名(定位 key/iv 位置)

1
secretKey  aesKey  secret  iv  privateKey  publicKeynonce  timestamp  appSecret setpublicKey

第三层:CryptoJS 特征函数(搜到基本就锁定加密逻辑)

1
AES.encrypt  AES.decrypt  Utf8.parse  mode.CBC  pad.Pkcs7MD5(  HmacSHA256  enc.Hex  enc.Base64

第四层:请求包装层(找统一加解密入口)

1
interceptor  interceptors  request.use  response.usewx.request  axios.create  useEncryptHook

第五层:正则搜密钥本体(Console 或全局搜)

1
[0-9a-f]{32}      → AES-256 key / MD5 值[0-9a-f]{16}      → AES-128 key / IV-----BEGIN        → RSA 公私钥
流量特征大概率是什么搜索方向
Base64 长串,长度是 16 的倍数AESAES、encrypt、key、iv
JSON 里带 sign + timestamp/nonce签名防重放sign、MD5、HmacSHA
超长 Base64(几百字节)RSAJSEncrypt、setPublicKey
前端国密项目(政企/银行常见)SM4/SM2sm4、sm2、gm
请求和响应都是密文双向加密(hook 点要两个)decrypt 也要搜

前端堆栈原理与三大定位技巧

堆栈原理:

调用堆栈(Call Stack):当JS代码执行时,每调用一个函数,浏览器会将该函数的执行上下文(如参数、局部变量、执行位置)压入“栈”中;函数执行完毕后,会从栈中弹出。最终,堆栈会记录当前所有未执行完毕的函数调用关系。 栈的特性:后进先出(LIFO):最后被调用的函数会压在栈顶,最先执行完毕并弹出;最先被调用的函数压在栈底,最后执行完毕。

堆栈与执行顺序的关系:堆栈展示顺序:从上到下 = 调用顺序(后调用的函数在栈顶);

1790258930917

全局搜索:最快定位已知特征的加密函数

前端加解密逻辑往往包含明显的特征关键词,如:

函数/参数名:encrypt(加密)、decrypt(解密)、sign(签名)、password(密码)、salt(盐值)、key(密钥); 算法关键词:MD5、SHA256、AES、RSA、CryptoJS(常用加密库); 操作关键词:base64、encode(编码)、decode(解码)。

1790259308217

事件断点:追溯用户操作触发的加密逻辑

事件断点适用于加密逻辑由用户操作触发的场景(如点击登录按钮、提交表单、输入文本),无需提前知道任何代码特征,通过“捕获用户事件”反向追溯加密函数。

核心原理 用户操作(点击、输入、提交)会触发DOM事件(click、submit、input),而加解密逻辑通常在事件的回调函数 中执行(如点击登录按钮后,回调函数中获取表单数据并加密)。通过给DOM元素或全局事件设置断点,可在事件触发时暂停代码,再通过堆栈追溯到加密函数。 两种事件断点用法:

1)元素级事件断点(精准) 适用于已知触发加密的具体DOM元素(如登录按钮、提交按钮):

切换到「Elements」面板,通过「Ctrl+Shift+C」选中目标元素(如登录按钮

切换到「Sources」面板,展开「Event Listener Breakpoints」; 勾选对应事件类型(如「Mouse → click」、「Form → submit」); 触发用户操作,代码会暂停在所有click或submit事件的回调函数中; 结合堆栈和代码逻辑,筛选出与加解密相关的回调函数。 image

XHR/Fetch断点:从请求出发逆向加密源头

XHR/Fetch断点适用于已知加密参数所在接口的场景(如登录接口/user/login、数据接口/api/data),核心是“拦截请求发送过程”,通过堆栈逆向追溯参数加密的源头。(也可以全局搜索定位对应接口)

所有前端请求(XHR或Fetch)发送前,必须先完成参数组装和加密(如将明文密码转为密文、生成sign签名)。给目标接口设置断点后,当JS执行到XMLHttpRequest.send()或fetch()时会自动暂停,此时堆栈中会保留从“加密函数”到“请求发送”的完整调用链,向上追溯即可找到加密逻辑。 操作步骤: 切换到「Sources」面板,点击「XHR/Fetch Breakpoints」标签; 点击「+」号,输入目标接口的路径(支持精确匹配和模糊匹配): 精确匹配:/user/login(仅拦截该接口); 模糊匹配:/api/(拦截所有包含/api/的接口); 触发请求(如点击登录、刷新页面),当请求即将发送时,代码会暂停在send()或fetch()处; 查看「Call Stack」调用堆栈,从栈顶(send函数)向上追溯,找到参数加密的关键函数。

debugger反调试绕过

前端使⽤debugger反调试的情况:

1790272265734

方法一:bp设置过滤把debugger替换为空

1790272372789

新增⼀条把响应包的debugger替换为空的规则

1790272418437

1790272431648

然后清除浏览器缓存,挂代理给bp再重新打开系统就可以绕过debugger(大部分情况)

方法二:使用火狐浏览器

1790272514406

方法三:添加脚本以忽略列表

示例:访问https://antispider8.scrape.center/

1790329228470

我们并没有设置任何断点,也没有执行任何额外的脚本,它就直接进入了断点模式。

这里通过 setInterval 循环,每秒执行 1 次 debugger 语句

我们可以右键行好,点击永不在此处暂停

发现成功绕过

1790329543564

方法4:替换文件

了解一下Overrides 面板的用法,利用它我们可以将远程的 JavaScript 文件替换成本地的 JavaScript 文件

我们将当前的 JavaScript 文件复制到文本编辑器中,删除或者直接注释掉 debugger 这个关键字

打开 Sources 面板下的 Overrides 面板,将修改后的完整 JavaScript 文件复制进去

替换完成之后,我们重新刷新网页,这时候就发现不会进入无限 Debugger 模式了。

方式5:抓包丢弃

这边如果加载过一次JS之后会无限生成debugger文件,那么我们在他加载之前直接drop掉不就可以了吗?说干就干

1790330642965

成功绕过

防重放绕过

首先要明确一个事情,重放攻击是二次请求,黑客通过抓包获取到了请求的HTTP报文,然后黑客自己编写了一个类似的HTTP请求,发送给服务器。也就是说服务器处理了两个请求,先处理了正常的HTTP请求,然后又处理了黑客发送的篡改过的HTTP请求。

防重放示例:timestamp和nonce

timestamp参数–因为一次正常的HTTP请求,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间相比较,是否超过了60s,如果超过了则认为是非法的请求。

nonce参数–nonce的意思是仅一次有效的随机字符串,要求每次请求时,该参数要保证不同,所以该参数一般与时间戳有关,实际使用时可以加上客户端的ip地址,mac地址等信息做个哈希之后,作为nonce参数。我们将每次请求的nonce参数存储到一个“集合”中,可以json格式存储到数据库或缓存中.每次处理HTTP请求时,首先判断该请求的nonce参数是否在该“集合”中,如果存在则认为是非法请求。

如果同时使用timestamp和nonce参数呢? nonce的一次性可以解决timestamp参数60s的问题,timestamp可以解决nonce参数“集合”越来越大的问题。

签名生成:

1
2
$sign=md5($uid.$token.$stime.$nonce);
// 服务器通过uid从数据库中可读出token

自动化绕过:

通过逆向js寻找sign签名生成逻辑,生成自动化生成脚本,使用mitmproxy工具进行绕过.至于mitmproxy工具后面也会详细讲解

1790271991621

⾃动化加解密过防重放-使⽤mitmproxy

mitmproxy介绍:

Mitmproxy是一个免费的开源交互式的HTTPS代理。MITM即中间人攻击

mitmproxy安装:

window通过pipx直接通过pip安装

1
2
pip install pipx
pipx install mitmproxy

mitmproxy安装验证和安装证书:

cmd中输入mitmweb查看是否安装成功

1790330793163

或者cmd查看版本号mitmproxy –version

1790330842693

安装证书:

此时,还没有安装成功,我们发现还不能拦截到数据,这是因为我们没有安装证书和打开对应的监听窗口

windows平台的证书其实在我们装mitmproxy的时候系统已经自动给你下载了

Window安装路径在我们的usr\15128( 用户名).mitmproxy 下,p12结尾的证书就是windows平

台下的。

1790330970905

1790330992158

这里的密码填空就行了

使用方式:

浏览器设置代理mitnweb对应的监听端口

1
mitmproxy --listen-port 8888 -s test.py

test.py是自己编写的

浏览器依旧是设置8080

1790331264532

抓包⼯具设置上层代理(代理端⼝就是mitmproxy监听的端⼝),监听 8080

1790331702468

设置*就是所有⽹站都⾛代理,测试中建议只设置⽬标系统的地址⾛代理就⾏了

正向(请求出去):

  1. 浏览器发起 HTTP/HTTPS 请求,浏览器代理配置:127.0.0.1:8080,请求发给 Burp
  2. Burp 收到流量,Burp 配置了上游代理(Upstream Proxy)指向 127.0.0.1:8888
  3. Burp 把流量转发给 mitmproxy(监听 8888)
  4. mitmproxy 加载了test.py脚本,触发脚本的request函数,自动修改请求(加 header、改参数等)
  5. mitmproxy 将处理后的请求,发送到远端目标网站服务器

反向(响应回来):

  1. 目标网站返回响应数据包,发给 mitmproxy
  2. mitmproxy 触发脚本response函数,可以修改返回内容
  3. mitmproxy 把修改后的响应包回传给 Burp
  4. Burp 接收响应,在 Burp 界面展示请求和响应,你可以手动查看 / 二次修改
  5. Burp 把响应交给浏览器

text.py示例:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# encoding=utf-8
import uuid
import hashlib
import json
from mitmproxy import ctx
import time
import random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.Padding import unpad
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
import base64
# 加解密处理
# 设置AES密钥和初始向量
Key= "7d8251684679455491db4d2e3c85e521"
key = b"7d8251684679455491db4d2e3c85e521"
iv = b"2098634618552512"

#AES加密函数
def aes_encrypt(data):
    print('执行了加密')
    """
    AES 加密
    :param data: 需要加密的字符串,先进行AES加密然后再BS64加密
    :return: 返回加密后字符串
    """
    cipher = AES.new(key, AES.MODE_CBC, iv)
    padded_data = pad(data.encode('utf-8'), AES.block_size, style='pkcs7')
    encrypted_data = cipher.encrypt(padded_data)
    return base64.b64encode(encrypted_data).decode('utf-8')

#AES解密函数
def aes_decrypt(data):
    print('执行了解密')
    """
    AES 解密
    :param s: 加密后的字符串,先BS64解密再AES解密
    :return:  返回解密后的字符串
    """
    cipher = AES.new(key, AES.MODE_CBC, iv)
    encrypted_data = base64.b64decode(data)
    decrypted_data = cipher.decrypt(encrypted_data)
    unpadded_data = unpad(decrypted_data, AES.block_size, style='pkcs7')
    return unpadded_data.decode('utf-8')

#请求头AES密钥进行RSA加密
# 加载RSA公钥
public_key_pem = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCUIPx7My9gtdfVuobe1FDwCIjZ
LUg6T7e5XDoJyDzX4RgrM279jSOz98cXI8iLjqW7d+OTlA3K1yUL/w03eaLTEiqp
COkbK3HIWoJ5uK+oPzGYTvixHDIqn6z159nEZRlDOxRgYnf3b8G63fmIUv9ZBCYq
1gsl/f6se9QX6Z/SzwIDAQAB
-----END PUBLIC KEY-----
"""
public_key = serialization.load_pem_public_key(public_key_pem.encode(), backend=default_backend())

# RSA加密函数
def RSA_Encrypt(data):
    encrypted_data = public_key.encrypt(
        data.encode(),
        padding.PKCS1v15()
    )
    return base64.b64encode(encrypted_data).decode('utf-8')

class ReplayBreakdown:
    def __init__(self):
        pass
    def request(self, flow):
        if "ctzhgd.eshore.cn" == flow.request.host:
            # nonce生成
            uuids = str(uuid.uuid4())
            nonce = uuids.replace("-", "") # 去掉UUID中的`-`
            # print("nonce:", nonce)
            # timestamp生成
            timestamp = str(int(round(time.time() * 1000)))
            # print("timestamp:", timestamp)
            # 获取原始请求的未加密参数
            data = flow.request.get_text()
            datas = "data="+data+"&nonce="+nonce+"&timestamp="+timestamp
            #生成sign签名
            # 创建一个SHA-1哈希对象,实现sha1加密
            sha1 = hashlib.sha1()
            sha1.update(datas.encode('utf-8') if isinstance(datas, str) else datas)
            signature=sha1.hexdigest()
            print("signature:", signature)
            flow.request.headers["Nonce"] = nonce
            flow.request.headers["Timestamp"] = timestamp
            flow.request.headers["Key"] = RSA_Encrypt(Key)
            # flow.request.headers["Key"] = 'kU6iZsk/Xi/+IfeKBm7GTbKZSHOq673Hb0c+qixTyNZNkb7TU2Mh1HEeYAJV/VeHYvZW1EOdo3xOPNv1IcP1OpCTzYsWWpTkAhbmdvZ5TBAgU85FvgKVqE6IdEulpkfx7zU6e+7ra0YgTq2tumBmyrojBAC5HUYQdBLQlyBij9E='
            flow.request.headers["Signature"] =signature
            #请求数据处理
            ecrypt_data  = aes_encrypt(data)
            print("ecrypt_data:", ecrypt_data)
            # 修改请求体
            flow.request.content = f'{{"data":"{ecrypt_data}"}}'.encode()

    # def response(self, flow):
    #     # 检查是否需要解密,例如检查URL或其他条件
    #     if "ctzhgd.eshore.cn" == flow.request.host:
    #         # 获取返回的加密数据
    #         response_data = json.loads(flow.response.content)
    #         # 对返回的加密数据进行解密
    #         data_rep = response_data.get("data")
    #         decrypt_data = aes_decrypt(data_rep)
    #         # 打印解密后的数据
    #         flow.response.content = decrypt_data.encode('utf-8')  # 确保是字节格式
addons = [ReplayBreakdown()]
本文由作者按照 CC BY 4.0 进行授权