抓包分析

目标是某招聘类小程序的面试经验数据:

通过观察Fiddler 抓到的数据包,我们可以看到这款小程序对两个地方进行了加密,一个是请求参数 QueryString 里面的 bkiv

一个是 Response 里面的文本数据:

由于小程序没有像PC端那样有js文件渲染,所以推断是内部的wxapkg文件对数据进行的加解密,因此就需要反编译获取小程序的源码,再进行静态调试分析算法。

源码获取

wxapkg文件的获取

PC端微信的小程序包缓存路径,一般为 .\WeChat Files\Applet,里面每个 wx 开头的文件夹代表一个小程序,为了锁定目标的小程序,可以先清空 Applet 目录,然后打开小程序,找到目录下唯一的 wx 文件夹,主包 __APP__.wxapkg 就是我们要分析的对象了

反编译(参考链接

反编译前,使用UnpackMiniApp.exe对小程序包进行解密

  • 如果提示安装 Framwork 3.5 失败,重启电脑即可

  • 解密前,需要在根目录下创建好 wxpack 文件夹,不然会报错


接下来就是对解密后的包文件反编译了,cmd打开命令窗口, 进入到 wxappUnpacker-master 目录,运行 node wuWxapkg.js 命令 (需要配置好node环境)

输出成功结果

在 wxpack 中找到反编译后的包源码

源码分析

打开小程序项目源码,直接搜索参数名 kiv,在第二个搜索结果找到加密点,使用的算法疑似为 AES 加密。进入到 i.encryptAES 函数内部,可以看到由 r 对象调用了 encrypt 方法进行了加密

再进入到 r 对象内部,即 crypto-config.js 文件,根据红框处代码,基本可以确定加密算法为 AES,加密模式为CBC,填充为 pkcs7

下一步来看看 key 和 iv 是如何生成的,首先是 key:

可以看到,key 即为 e 经过 utf8 编码所得,拿到 e 的生成代码,到 node 环境或者 chrome console 运行:

分别补充 a, n, d 参数:

再次运行 e 的生成代码,得到密钥 key:

接着来看看 iv 是如何生成的,在第一步搜索 kiv 的时候,注意到有一个i.generateIv 函数,猜测是每次用来生成随机 iv:

进入其内部,找到生成算法:

这段js代码的大致意思就是每次随机从ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 取一个字符,然后拼成一个16位长度的字符串

还有一个地方需要注意的,b 参数是数据经过加密后,再进行字符的替换生成的结果:

具体的替换规则就是:

/ 替换为 _
+ 替换为 -
= 替换为 ~

知道加密算法以及所需参数后,就可以使用现成的工具对加密内容进行解密,来验证猜想,比如 b 参数:

解密结果:

解密Response里面的文本数据:

解密结果:

可以看到两个都解密成功了

python还原加密算法

首先是封装 AES 工具类,由于密钥 key 是固定的,因此每次创建实例的时候,只需要传入 iv 参数就行

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
import base64

from Crypto.Cipher import AES


class AESTool:
def __init__(self, iv):
self.key = "G$$QawckGfaLB97r".encode("utf-8")
self.iv = iv.encode("utf-8")

def pkcs7padding(self, text):
"""
明文使用PKCS7填充
"""
bs = 16
length = len(text)
bytes_length = len(text.encode("utf-8"))
padding_size = length if (bytes_length == length) else bytes_length
padding = bs - padding_size % bs
padding_text = chr(padding) * padding
self.coding = chr(padding)
return text + padding_text

def aes_encrypt(self, content: str):
"""
AES加密
"""
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
# 处理明文
content_padding = self.pkcs7padding(content)
# 加密
encrypt_bytes = cipher.encrypt(content_padding.encode("utf-8"))
# 重新编码
result = str(base64.b64encode(encrypt_bytes), encoding="utf-8")
return result

def aes_decrypt(self, content: str):
"""
AES解密
"""
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
content = base64.b64decode(content)
text = cipher.decrypt(content).decode("utf-8")
return self.pkcs7padding(text)

iv 的生成函数,主要通过 python 内置的 random 模块的 choice 方法,来实现随机性

1
2
3
4
5
from random import choice

def gen_aes_iv():
seeds = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
return "".join(choice(seeds) for _ in range(16))

文本特殊符号的替换和 aes 加解密函数,两个函数都接收一个 aes 实例,作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def encrypt_raw_params(params, aes_tool, iv):
b = str(params).replace(" ", "").replace("'", '"')
enc_params = {
"b": aes_tool.aes_encrypt(b)
.replace("/", "_")
.replace("+", "-")
.replace("=", "~"),
"kiv": iv,

}
return enc_params

def decrypt_enc_text(cipher_text, aes_tool):
plain_text = aes_tool.aes_decrypt(
cipher_text.replace("_", "/").replace("-", "+").replace("~", "=")
)
json_text = re.search(r"{.+}", plain_text).group(0)
data = json.loads(json_text)
return data

分析的流程和算法的还原就到这了,具体的数据获取就不演示了,只要加密方式不出错,就是一个request请求的事,注意好请求包ua什么的隐藏就行

2022/9/8 更新

动态调试

参考:https://paker.net.cn/article?id=13

使用微信开发者工具导入反编译后的 wx…… 文件夹,即可像调试网页一样

小程序抓不到包:

原因:PC端微信架构发生改变,使用 3.6 及以下版本即可

「微信电脑版官方最新版本下载|微信电脑版历史软件版本下载大全」-天极下载

历史版本下载https://github.com/tom-snow/wechat-windows-versions/releases?page=2

Fiddler抓包问题,如何抓电脑版微信上的小程序的包? - 知乎

具体操作:

  1. fiddler配置全局代理
  2. 安装 3.6 版本微信
  3. 打开目录:C:\Users\chenxiaosheng\AppData\Roaming\Tencent\WeChat\XPlugin\Plugins\WMPFRuntime
  4. 清空目录,重启微信