Chrome游览器前端代码调试解析密码加密逻辑

1.在登录接口开启全局监听事件

1.1.开启全局监听事件的断点

​ 此时只要我们鼠标点击任何按钮,代码会停止运行,此时可进入调试模式。

image-20230714174605948

1.2.狂按F9进行单步调试(接近200次)

​ 由于我并不熟悉该系统代码逻辑,因此我不能跳过任何一个函数,以防错过关键信息。

1.3.经过长时间的单步代码调试来到了user.js的代码逻辑中

​ 这里有我想要的关键信息,通过响应体里获取到了关键的publicKey,通过变量名我们大致可以判断,是非对称加密方式,因此我们大致可以猜测是SM2,或者是RSA。

image-20230714175302667

1.4.密码算法判断

​ 由于SM2通常是512位的公钥,而RSA密钥长度至少1024位以上,这长度明显比512位更长因此再次确认位rsa公钥。

​ 在密码加密逻辑中变量命令为sm3?

1.6.为了进一步确认,我需要更加严谨的判断,再次进行单步调试。

image-20230714181514493

1.7.将密码转换为字节数组

image-20230714181650454

该函数的逻辑我们知道了,那么按F11跳出这个函数继续下一步。

1.8.通过对密码算法的具体判断

通过对之后密码算法的具体分析,确实是SM3加密,

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
function sm3(array) {
let len = array.length * 8

// k 是满足 len + 1 + k = 448mod512 的最小的非负整数
let k = len % 512
// 如果 448 <= (512 % len) < 512,需要多补充 (len % 448) 比特'0'以满足总比特长度为512的倍数
k = k >= 448 ? 512 - (k % 448) - 1 : 448 - k - 1

// 填充
const kArr = new Array((k - 7) / 8)
const lenArr = new Array(8)
for (let i = 0, len = kArr.length; i < len; i++) kArr[i] = 0
for (let i = 0, len = lenArr.length; i < len; i++) lenArr[i] = 0
len = len.toString(2)
for (let i = 7; i >= 0; i--) {
if (len.length > 8) {
const start = len.length - 8
lenArr[i] = parseInt(len.substr(start), 2)
len = len.substr(0, start)
} else if (len.length > 0) {
lenArr[i] = parseInt(len, 2)
len = ''
}
}
const m = new Uint8Array([...array, 0x80, ...kArr, ...lenArr])
const dataView = new DataView(m.buffer, 0)

// 迭代压缩
const n = m.length / 64
const V = new Uint32Array([0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600, 0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e])
for (let i = 0; i < n; i++) {
W.fill(0)
M.fill(0)

// 将消息分组B划分为 16 个字 W0, W1,……,W15
const start = 16 * i
for (let j = 0; j < 16; j++) {
W[j] = dataView.getUint32((start + j) * 4, false)
}

// W16 ~ W67:W[j] <- P1(W[j−16] xor W[j−9] xor (W[j−3] <<< 15)) xor (W[j−13] <<< 7) xor W[j−6]
for (let j = 16; j < 68; j++) {
W[j] = (P1((W[j - 16] ^ W[j - 9]) ^ rotl(W[j - 3], 15)) ^ rotl(W[j - 13], 7)) ^ W[j - 6]
}

// W′0 ~ W′63:W′[j] = W[j] xor W[j+4]
for (let j = 0; j < 64; j++) {
M[j] = W[j] ^ W[j + 4]
}

// 压缩
const T1 = 0x79cc4519
const T2 = 0x7a879d8a
// 字寄存器
let A = V[0]
let B = V[1]
let C = V[2]
let D = V[3]
let E = V[4]
let F = V[5]
let G = V[6]
let H = V[7]
// 中间变量
let SS1
let SS2
let TT1
let TT2
let T
for (let j = 0; j < 64; j++) {
T = j >= 0 && j <= 15 ? T1 : T2
SS1 = rotl(rotl(A, 12) + E + rotl(T, j), 7)
SS2 = SS1 ^ rotl(A, 12)

TT1 = (j >= 0 && j <= 15 ? ((A ^ B) ^ C) : (((A & B) | (A & C)) | (B & C))) + D + SS2 + M[j]
TT2 = (j >= 0 && j <= 15 ? ((E ^ F) ^ G) : ((E & F) | ((~E) & G))) + H + SS1 + W[j]

D = C
C = rotl(B, 9)
B = A
A = TT1
H = G
G = rotl(F, 19)
F = E
E = P0(TT2)
}

V[0] ^= A
V[1] ^= B
V[2] ^= C
V[3] ^= D
V[4] ^= E
V[5] ^= F
V[6] ^= G
V[7] ^= H
}

// 转回 uint8
const result = []
for (let i = 0, len = V.length; i < len; i++) {
const word = V[i]
result.push((word & 0xff000000) >>> 24, (word & 0xff0000) >>> 16, (word & 0xff00) >>> 8, word & 0xff)
}

return result
}

1.9.确实是SM3加密,为什么会有公钥?

通过直接对结果分析确实是SM3,且只进行了SM3加密,未进行RSA(代码逻辑与测试时代码逻辑不同),改完的代码逻辑虽然请求获取到了公钥但是公钥全程都没有使用。

image-20230717093105746

image-20230717093220898

1.10.思考

​ 这样只进行SM3加密没有多大意义,攻击者截获数据包之后,完全不需要知道原密码是什么,只需要抓包改包即可,还不如RSA加密,因为RSA加密至少每次解密结果都不同。而且这种SM3哈希摘要完全可以把加密串在本地使用彩虹表爆破。最安全的方式应该是先进行SM3然后再进行一次RSA加密,反正原本的代码逻辑就有RSA加密,只是中间加了一个SM3而已,这种修改方式即安全又方便。

(该环境明显不是生产环境而是开发环境,真实的生产环境中JS代码都是压缩之后的代码很难看懂代码含义,这个代码逻辑太清晰了,是开发环境,实际测试大多情况chrome前端可以看到的代码都是压缩后的代码,因此不是每次都可以成功获取到密码加密逻辑。)