MySQL協議分析之握手認證

介紹

每次使用MySQL的時候,都是直接使用編寫好的驅動,只關注業務部分。此次想探索一下鏈接的過程,所以有了此次總結。html

與MySQL服務器的交互,能夠分爲四個階段mysql

  1. 創建TCP鏈接
  2. 握手認證階段
  3. 命令執行階段
  4. 斷開鏈接

這裏主要探索握手認證的階段,注意這裏的握手認證,和TCP的三次握手不是同一個,是先創建了TCP鏈接,即已經完成了TCP三次握手,才進入到MySQL的握手認證。git

MySQL數據報文固定格式

MySQL的數據報文,由固定4byte的消息頭消息體組成。 其中消息頭的前3個byte表明消息體的實際大小,第4個byte表明序號,用於保證消息順序的正確。github

解析握手包

當客戶端與MySQL服務器創建起TCP鏈接的時候,MySQL服務器會發送一個握手包給客戶端,稱爲HandshakeV10,來分析一下這個報文結構:sql

大小(單位byte) 名稱 解釋
1 protocol version 協議版本號
n server version MySQL服務器版本,string[NUL]類型
4 connection id 服務器線程ID
8 auth-plugin-data-part-1 挑戰隨機數第一部分
1 filler 1byte的填充數,值爲0x00
2 capability flags 服務器權能標誌,低16位
1 character set 字符編碼
2 status flags 服務器狀態
2 capability flags 服務器權能標誌,高16位
1 length of auth-plugin-data 挑戰數長度
10 reserved 10byte的填充數,值爲0x00
13 auth-plugin-data-part-2 挑戰隨機數第二部分,最少12字節,最長13字節
n auth-plugin name 驗證插件的名稱,string[NUL]類型

string[NUL] 以0x00結尾的字符串 數據庫

首先MySQL採用的是挑戰/應答的模式進行驗證,所以獲取到這個握手包以後,要對該報文進行解析,獲取到挑戰隨機數,用於下個步驟中進行應答。服務器

定義一個結構體 HandsharkProtocol數據結構

type HandsharkProtocol struct {
	ProtocolVersion     byte
	ServerVersion       string
	ConnectionId        uint32
	AuthPluginDataPart1 []byte
	Filler              byte
	CapabilityFlag1     []byte
	CharacterSet        byte
	StatusFlags         []byte
	CapabilityFlag2     []byte
	AuthPluginDataLen   byte
	Reserved            []byte
	AuthPluginDataPart2 []byte
	AuthPluginName      string
}
複製代碼

解析握手包的過程,就按照定義,逐個解析app

func DecodeHandshark(b []byte) *HandsharkProtocol {
	hs := HandsharkProtocol{}
	buff := bytes.NewBuffer(b) // 將[]byte轉成buffer,方便處理
	buff.Next(4) // 前四個字節爲消息頭,不處理
	hs.ProtocolVersion, _ = buff.ReadByte() // 協議版本號
	hs.ServerVersion, _ = buff.ReadString(0x00) // MySQL服務器版本 0x00結尾的字符串
	hs.ConnectionId = binary.LittleEndian.Uint32(buff.Next(4)) // 服務器線程ID 小端法存儲
	hs.AuthPluginDataPart1 = buff.Next(8) // 挑戰隨機數第一部分
	hs.Filler, _ = buff.ReadByte() // 1byte的填充
	hs.CapabilityFlag1 = buff.Next(2) // 服務器權能標誌 低16位

	if buff.Len() > 0 { // if more data in the packet
		hs.CharacterSet, _ = buff.ReadByte() // 字符編碼
		hs.StatusFlags = buff.Next(2) // 服務器狀態
		hs.CapabilityFlag2 = buff.Next(2) // 服務器權能標誌 高16位
		hs.AuthPluginDataLen, _ = buff.ReadByte() // 挑戰數長度
		hs.Reserved = buff.Next(10) // 10 byte的填充
		hs.AuthPluginDataPart2 = buff.Next(12) // 挑戰隨機數第二部分
		buff.ReadByte() // 挑戰隨機數第13個byte,0x00 所以無視
		hs.AuthPluginName, _ = buff.ReadString(0x00) // 驗證插件的名稱 0x00結尾的字符串
	}

	return &hs
}
複製代碼

來測試一下,解析出來的握手包結構tcp

func main() {
	c, err := net.Dial("tcp", ":3306")
	if err != nil {
		fmt.Println(err)
	}
	pkg := make([]byte, 1024)
	_, _ = c.Read(pkg)
	h := DecodeHandshark(pkg)
	fmt.Println(h)
}
複製代碼

h 爲 &{10 8.0.19 421 [56 49 17 70 10 105 114 72] 0 [255 255] 255 [2 0] [255 199] 21 [0 0 0 0 0 0 0 0 0 0] [24 87 2 88 38 4 19 40 89 59 84 62] caching_sha2_password},協議版本爲10,服務器版本8.0.19等等信息均可以直觀的看到,接下來就是進行驗證應答。

發送握手包響應數據

首先看看響應握手包的數據結構HandshakeResponse41

大小(單位byte) 名稱 解釋
4 capability flags 客戶端全能標誌位,通常值爲CLIENT_PROTOCOL_41(0x00000200)
4 max-packet size 包的最大長度,能夠設置成0x00
1 character set 字符編碼
23 reserved 23byte的填充數,值爲0x00
n username 要登陸的用戶名 string[NUL]型
1 length of auth-response 應答挑戰數的長度
n auth-response 應答挑戰數
n database 要鏈接的數據庫名稱 string[NUL]型
n auth plugin name 驗證插件名稱

因爲採用的是caching_sha2_password驗證的方式,所以根據該方式,hash數據庫的登陸密碼

// 這段代碼是使用 github.com/go-sql-driver/mysql包中的方法
func scrambleSHA256Password(scramble []byte, password string) []byte {
	if len(password) == 0 {
		return nil
	}

	crypt := sha256.New()
	crypt.Write([]byte(password))
	message1 := crypt.Sum(nil)

	crypt.Reset()
	crypt.Write(message1)
	message1Hash := crypt.Sum(nil)

	crypt.Reset()
	crypt.Write(message1Hash)
	crypt.Write(scramble)
	message2 := crypt.Sum(nil)

	for i := range message1 {
		message1[i] ^= message2[i]
	}

	return message1
}
複製代碼

有了這個基礎以後,就能夠開始構造報文了

func GetHandshakeResponsePacket(authResp []byte, plugin string) []byte {
	//消息體的長度
	authLen := len(authResp)
	pkgBodyLen := 4 + 4 + 1 + 23 + len(user) + 1 + authLen + len(authResp) + len(dbName) + 1 + len(plugin) + 1
	data := make([]byte, pkgBodyLen + 4) // + 4的消息頭長度

	// capability flags
	var capabilityFlags uint32 = 512 | 8 | 524288 
	// 客戶端權能標誌位
	// 512 表明 clientProtocol41,
	// 8 表明 clientConnectWithDB, 
	// 524288 表明 clientPluginAuth
	// 使用小端法表示,參照 binary.LittleEndian.PutUint32() 方法
	data[4] = byte(capabilityFlags)
	data[5] = byte(capabilityFlags >> 8)
	data[6] = byte(capabilityFlags >> 16)
	data[7] = byte(capabilityFlags >> 24)

	// max-size
	data[8] = 0x00
	data[9] = 0x00
	data[10] = 0x00
	data[11] = 0x00

	// character set
	data[12] = 255 // 255 對應 utf-8

	// reserved
	pos := 13
	for ; pos < 13 + 23; pos++ {
		data[pos] = 0
	}

	//username
	pos += copy(data[pos:], user)
	data[pos] = 0x00
	pos++

	// authResp
	pos += copy(data[pos:], []byte{byte(uint64(authLen))})
	pos += copy(data[pos:], authResp)

	// dbName
	pos += copy(data[pos:], dbName)
	data[pos] = 0x00
	pos++

	// plugin
	pos += copy(data[pos:], plugin)
	data[pos] = 0x00
	pos++

	return data[:pos]
}
複製代碼

最後再補上消息頭,就能夠發送給MySQL服務器

const user = "root" // 用戶名
const password = "123" // 密碼
const dbName = "test" // 數據庫名

func main() {
	c, err := net.Dial("tcp", ":3306")
	if err != nil {
		fmt.Println(err)
	}
	pkg := make([]byte, 1024)
	_, _ = c.Read(pkg)
	h := DecodeHandshark(pkg)

        // 構造響應報文
	scramble := append(h.AuthPluginDataPart1, h.AuthPluginDataPart2...)
	authResp := scrambleSHA256Password(scramble, password)
	respPacket := GetHandshakeResponsePacket(authResp, h.AuthPluginName)
	pktLen := len(respPacket) - 4 // 減掉消息頭長度
	respPacket[0] = byte(pktLen)
	respPacket[1] = byte(pktLen >> 8)
	respPacket[2] = byte(pktLen >> 16) // 前3個byte小端法表示消息體大小
	respPacket[3] = 1 // 序號
	c.Write(respPacket)
	
	// 這兩句爲了阻塞程序,方便查看是否已經鏈接上mysql
	ch := make(<-chan int, 1) 
	<-ch 
}
複製代碼

確認認證結果,完成認證

最後來看看是否鏈接成功,在程序沒有鏈接以前,mysql服務器裏只有兩個客戶端鏈接

而後運行程序

能夠看到多了一個客戶端鏈接,說明握手認證成功,以後就能夠發送命令給mysql執行。

總結

雖然在日常的工做中,不會本身編寫鏈接驅動,可是秉着好奇的心,研究了一下mysql的鏈接過程,仍是受益頗多~。

Thanks!

相關文章
相關標籤/搜索