实现自己的数据库驱动——MySQL协议Auth Packet解析(四)

前言

上一篇简单介绍了HandShake包的数据格式以及利用WireShark如何去分析。这篇要介绍的是Auth Packet,即客户端认证包的数据格式。

Auth Packet主要是发送用户的信息、密码、Schema等,其中密码需要HandShake中Salt。所以在发送Auth Packet,需要先解析出Salt,然后对密码进行加密,之后再一起打包发送。

Auth Packet数据格式

与HandShake相同,前面的四个字节是包头,其中前三位代表Handshake packet数据长度,第四位是包序列号。

以下是Auth四字节后的内容。

相对包内容的位置 长度(字节) 名称 描述
0 2 协议协商 用于与服务端协商通讯方式
2 2 扩展的协议 用于与服务端协商通讯方式
4 4 消息最长长度 客户端可以发送或接收的最长长度,0表示不做任何限制
8 1 字符编码 客服端字符编码方式
9 23 保留字节 未来可能会用到,预留字节,用0代替
32 不定长 认证字符串 用户名:NullTerminatedString格式编码
不定 不定长 认证密码 加密后的密码:LengthEncodedString格式编码
不定 不定长 数据库名称 NullTerminatedString格式编码

NullTerminatedString(Null结尾方式): 字符串以遇到Null作为结束标志,相应的字节为0x00

LengthEncodedString(指定字符串长度方式)

在网上看到的协议协商是4个字节的,MySQL8.x的协议协商被拆成了两部分。用两个字节表示多出来了扩展的协议。

Wireshark分析Auth Packet

我还是用WireShark来分析一下Auth Packet的数据格式。点击Login Request user=root db=data可以看到Auth包的具体内容。

你想输入的替代文字

这里着重解释一下Password,首先这个数据长度是指定的,即图中显示为20个字节。而这个加密的密码是根据HandShake中的两个Salt来的,HandShake中第一个Salt是8位的,第二个Salt是12位的。根据两个Salt最后等到加密后的密码。

这部分内容是由客户端自己生成,所以说如果我们如果要写一个程序连接数据库,那么这个包就得按照这个格式,不然服务端将会无法识别。

加密方式

生成Auth Packet的过程中,需要将密码加密再发送。MySQL4.1x之前的版本采用的是323加密方式,4.1.x之后采用了411的加密方式。这块加密还不够熟悉,所以后面需要补一下。不过在网上可以找到对应的代码。

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
//411是mysql4.1版本之后采用的加密方式
public static final byte[] scramble411(byte[] pass, byte[] seed)
throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] pass1 = md.digest(pass);
md.reset();
byte[] pass2 = md.digest(pass1);
md.reset();
md.update(seed);
byte[] pass3 = md.digest(pass2);
for (int i = 0; i < pass3.length; i++) {
pass3[i] = (byte) (pass3[i] ^ pass1[i]);
}
return pass3;
}

//323是MySQL4.1x版本之前采用的加密方式
public static final String scramble323(String pass, String seed) {
if ((pass == null) || (pass.length() == 0)) {
return pass;
}
byte b;
double d;
long[] pw = hash(seed);
long[] msg = hash(pass);
long max = 0x3fffffffL;
long seed1 = (pw[0] ^ msg[0]) % max;
long seed2 = (pw[1] ^ msg[1]) % max;
char[] chars = new char[seed.length()];
for (int i = 0; i < seed.length(); i++) {
seed1 = ((seed1 * 3) + seed2) % max;
seed2 = (seed1 + seed2 + 33) % max;
d = (double) seed1 / (double) max;
b = (byte) java.lang.Math.floor((d * 31) + 64);
chars[i] = (char) b;
}
seed1 = ((seed1 * 3) + seed2) % max;
seed2 = (seed1 + seed2 + 33) % max;
d = (double) seed1 / (double) max;
b = (byte) java.lang.Math.floor(d * 31);
for (int i = 0; i < seed.length(); i++) {
chars[i] ^= (char) b;
}
return new String(chars);
}