一个Drcom客户端的实现

本文仅适用韶关学院Drcom客户端。

因为自己需要经常用到Linux的缘故,所以打算把Linux上的网络问题搞定。学校使用的是Drcom认证客户端,在学校MIS系统上也提供了对应的Linux版本。可惜的是把一切依赖都弄好终于能运行后却总是秒断。这个程序并没有提供源码,我也无法获知是什么问题,所以打算干脆自己写一个,也就刚好分析一下Drcom的协议。

0x00 工作准备

  • 抓包软件Wireshark
  • 反汇编器IDA Pro
  • 能正常使用的Windows版Drcom客户端

0x01 EAP协议

简单的实验可以发现,在没有登陆内网前,机器并不能Ping通网关,也就是说认证的方式应该是通过广播的方式来进行的。通过Wireshark抓包可以得到几个重要的包:

1
2
3
4
5
6
7
8
EAPOL 96 Start 04:44:23.334841000
0000 ff ff ff ff ff ff 28 d2 44 2d 90 69 88 8e 01 01 ......(.D-.i....
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

协议的类型为0x888e,也即EAP,可扩展身份验证协议,是802.1x认证机制的核心。而EAPOL则是基于局域网的EAP。当我们的机器开始登录时,首先会向广播地址发出一个Start包,同在一个局域网的网关便会收到这个Start包并回复:

1
2
3
4
5
6
EAP 60 Request, Identity 04:44:23.335480000
0000 28 d2 44 2d 90 69 58 6a b1 56 78 00 88 8e 01 00 (.D-.iXj.Vx.....
0010 00 05 01 01 00 05 01 00 00 00 00 00 00 00 00 00 ................
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 98 a7 44 0c ..........D.

这个包即是向客户端请求一个Identity,也即身份信息。客户端收到这个request后,就应该向网关回应一个Identity:

1
2
3
4
5
6
7
8
EAP 96 Response, Identity 04:44:23.459779000
0000 ff ff ff ff ff ff 28 d2 44 2d 90 69 88 8e 01 00 ......(.D-.i....
0010 00 19 02 01 00 19 01 31 34 31 31 35 30 36 31 30 .......141150610
0020 32 34 00 44 61 00 00 c0 a8 c3 5f 00 00 00 00 00 24.Da....._.....
0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

这个包已经开始包含学号和IP地址,当网关收到这个Identity后,网关开始请求一个MD5-Challenge:

1
2
3
4
5
6
EAP 60 Request, MD5-Challenge EAP (EAP-MD5-CHALLENGE) 04:44:23.462349000
0000 28 d2 44 2d 90 69 58 6a b1 56 78 00 88 8e 01 00 (.D-.iXj.Vx.....
0010 00 1a 01 00 00 1a 04 10 19 54 6c 79 c0 a8 7f 81 .........Tly....
0020 c0 a8 7f 81 00 00 00 00 10 82 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 94 96 f5 b8 ............

这个包中包含了一串Challenge值(19 54 6c 79 c0 a8 7f 81 c0 a8 7f 81 00 00 00 00)。客户端收到这个值后,将其与密码做MD5运算,则可得到Challenged-Password。$$
C = md5(Id+Challenge+Password)
$$在Drcom中Id固定为0。
得到这串Challenged-Password后,客户端将其与学号一同发给网关

1
2
3
4
5
6
7
8
EAP 96 Response, MD5-Challenge EAP (EAP-MD5-CHALLENGE) 04:44:23.991527000
0000 ff ff ff ff ff ff 28 d2 44 2d 90 69 88 8e 01 00 ......(.D-.i....
0010 00 2a 02 00 00 2a 04 10 9e 47 e9 3a 45 c7 d8 6d .*...*...G.:E..m
0020 18 38 24 c0 20 91 2b 6c 31 34 31 31 35 30 36 31 .8$. .+l14115061
0030 30 32 34 00 44 61 24 00 c0 a8 c3 5f 00 00 00 00 024.Da$...._....
0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

网关收到这串Challenged-Password后进行验证,如果加密后的密码无误,则原密码也无误,就会发出一个Success包:

1
2
3
4
5
6
EAP 60 Success 04:44:24.010811000
0000 28 d2 44 2d 90 69 58 6a b1 56 78 00 88 8e 01 00 (.D-.iXj.Vx.....
0010 00 04 03 00 00 04 00 00 00 00 00 00 00 00 00 00 ................
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 55 00 41 e9 ........U.A.

整个认证握手过程到这里就结束了。此时使用PPPoE进行连接,发现已经可以连接到外网,也就表示认证已经成功。

0x02 认证过程的Go语言实现

Go语言中可以使用第三方包gopacket对数据包进行捕获和发送。但原版的gopacket在这个情景下使用会有些许问题,所以做了少许修改,并在文末附件给出。

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
/* 发送EAPOL包 */
func sendEAPOL(Version byte, Type layers.EAPOLType, SrcMAC net.HardwareAddr, DstMAC net.HardwareAddr) {
buffer := gopacket.NewSerializeBuffer()
options := gopacket.SerializeOptions{}
gopacket.SerializeLayers(buffer, options,
&layers.Ethernet{EthernetType: layers.EthernetTypeEAPOL, SrcMAC: SrcMAC, DstMAC: DstMAC},
&layers.EAPOL{Version: 0x01, Type: layers.EAPOLTypeStart},
)
//var err error
err := handle.WritePacketData(buffer.Bytes())
if err != nil {
fmt.Print(err)
os.Exit(0)
}
}
/* 发送EAP包 */
func sendEAP(Id uint8, Type layers.EAPType, TypeData []byte, Code layers.EAPCode, SrcMAC net.HardwareAddr, DstMAC net.HardwareAddr) {
buffer := gopacket.NewSerializeBuffer()
options := gopacket.SerializeOptions{}
gopacket.SerializeLayers(buffer, options,
&layers.Ethernet{EthernetType: layers.EthernetTypeEAPOL, SrcMAC: SrcMAC, DstMAC: DstMAC},
&layers.EAPOL{Version: 0x01, Type: layers.EAPOLTypeEAP, Length: uint16(len(TypeData) + 5)},
&layers.EAP{Id: Id, Type: Type, TypeData: TypeData, Code: Code, Length: uint16(len(TypeData) + 5)},
)
// err error
err := handle.WritePacketData(buffer.Bytes())
if err != nil {
fmt.Print(err)
os.Exit(0)
}
}

首选封装两个函数分别用来发送EAPOL包和EAP包,gopacket的用法请参考GoDoc

readNewPacket将从认证一开始就以一个协程运行,直至程序终止。在程序main函数中,有如下语句:

1
2
3
4
5
6
7
8
9
handle, err = pcap.OpenLive(devName, 1024, false, 5*time.Second)
defer handle.Close()
if err != nil {
fmt.Print(err)
os.Exit(0)
}
packetSrc := gopacket.NewPacketSource(handle, handle.LinkType())
go readNewPacket(packetSrc)

readNewPacket()会根据Code和Type分别进行处理。

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
/* 读取新数据包 */
func readNewPacket(packetSrc *gopacket.PacketSource) {
for packet := range packetSrc.Packets() {
eapl := packet.Layer(layers.LayerTypeEAP)
if eapl != nil {
switch eapl.(*layers.EAP).Code {
case 0x03: //Success
fmt.Println("Success")
sendPingStart()
case 0x01: //Request
switch int8(eapl.(*layers.EAP).Type) {
case 0x04: //EAP-MD5-CHALLENGE
//fmt.Println(eapl.(*layers.EAP).TypeData)
go responseMd5Challenge(eapl.(*layers.EAP).TypeData[1:17])
case 0x02: //Notification
fmt.Println("Failed")
os.Exit(0)
case 0x01: //Identity
//fmt.Print("TRUE")
go responseIndentity(eapl.(*layers.EAP).Id)
}
case 0x04: //Failure
fmt.Println("Failed")
fmt.Println("Retry...")
EAPAuth()
}
}
}
end <- true
}
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
/* EAP认证开始 */
func EAPAuth() {
fmt.Println(mac)
fmt.Println("EAP Start...")
sendEAPOL(0x01, layers.EAPOLTypeStart, mac, boardCastAddr)
}
/* EAP注销 */
func EAPLogoff() {
sendEAPOL(0x01, layers.EAPOLTypeLogOff, mac, boardCastAddr)
fmt.Println("Logoff...")
}
/* 回应身份(Indentity) */
func responseIndentity(id byte) {
dataPack := []byte{}
dataPack = append(dataPack, []byte(username)...) //用户名
dataPack = append(dataPack, []byte{0x00, 0x44, 0x61, 0x00, 0x00}...) //未知
dataPack = append(dataPack, ip[:]...) //客户端IP
fmt.Println("Response Identity...")
sendEAP(id, 0x01, dataPack, 2, mac, boardCastAddr)
}
/* 回应MD5-Challenge */
func responseMd5Challenge(m []byte) {
mPack := []byte{}
mPack = append(mPack, 0)
mPack = append(mPack, []byte(password)...)
mPack = append(mPack, m...)
mCal := md5.New()
mCal.Write(mPack)
dataPack := []byte{}
dataPack = append(dataPack, 16)
dataPack = append(dataPack, mCal.Sum(nil)...)
dataPack = append(dataPack, []byte(username)...)
dataPack = append(dataPack, []byte{0x00, 0x44, 0x61, 0x26, 0x00}...)
dataPack = append(dataPack, []byte(ip[:])...)
challenge = mCal.Sum(nil) //用于后面心跳包
fmt.Println("Response EAP-MD5-Challenge...")
sendEAP(0, 0x04, dataPack, 2, mac, boardCastAddr)
}

按照前文所说的认证方法分别对各种回应包进行构造,然后调用EAPAuth()函数即可。readNewPacket()函数将收到来自网关的request,并交由不同的response函数处理。

至此,可以构造出一个可用的认证程序,使用系统自带的PPPoE拨号程序即可。

0x03 问题的发现

使用这种认证方式虽然可以成功拨上号,但会发现隔一段时间就会自己掉线。通过Wireshark查看,发现网关主动发送了Failure包,也就是说网关注销了我们的账户。这种情况可以联想到使用官方客户端经常碰到的一个错误:

获取用户属性超时!请检查防火墙配置允许 UDP 61440 端口。

当客户端出现这个错误时,一般也可以上网,但一段时间后就会发现已经断开。所以我们重新打开校方提供的客户端,通过内网认证,并用wireshark筛选出UDP协议,端口为61440的所有数据包。

通过仔细查看会发现,客户端每隔一段时间会向一个服务器192.168.127.129:61440发送UDP报文,而服务器也会做出响应。也就是说,客户端应该是通过定时发送数据包来保持自己的在线状态,也就是所谓的心跳包。

0x04 心跳包的分析

(以下出现的数据仅为UDP数据部分,而非完整的数据包)

当客户端与网关完成认证流程后,客户端便开始向服务器192.168.127.129:61440发送报文:

1
0000 07 00 08 00 01 00 00 00 ........

一共只有八个字节,这部分是固定的,大部分Drcom心跳包都以07开头,其他部分含义未知。

然后服务器方面会回应一个报文:

1
2
0000 07 00 10 00 02 00 00 00 01 72 96 00 c0 a8 c3 5f .........r....._
0010 a8 ac 00 00 4f e4 16 c1 00 00 00 00 dc 02 00 00 ....O...........

具体含义我们暂且不管。收到这样的数据包后,客户端即发送一个数据包,包含用户的若干信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0000 07 01 f4 00 03 0b 28 d2 44 2d 90 69 c0 a8 c3 5f ......(.D-.i..._
0010 02 22 00 24 01 72 96 00 4a 6a 72 32 00 00 00 00 .".$.r..Jjr2....
0020 31 34 31 31 35 30 36 31 30 32 34 6c 7a 79 2d 70 14115061024lzy-p
0030 63 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c...............
0040 00 00 00 00 00 00 00 00 00 00 00 df 05 05 05 00 ................
0050 00 00 00 df 06 06 06 00 00 00 00 00 00 00 00 94 ................
0060 00 00 00 06 00 00 00 02 00 00 00 f0 23 00 00 02 ............#...
0070 00 00 00 44 72 43 4f 4d 05 b8 01 04 00 00 00 00 ...DrCOM........
0080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00b0 00 00 00 33 39 31 35 31 35 66 64 33 33 39 66 36 ...391515fd339f6
00c0 32 62 35 33 30 63 64 36 33 61 30 32 37 63 64 34 2b530cd63a027cd4
00d0 65 66 39 35 31 33 39 30 36 39 66 00 00 00 00 00 ef95139069f.....
00e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00f0 00 00 00 00 ....

具体分析如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
07 //固定头
01 //计数器,这里固定为01
00 f4 //报文长度,这个长度是不定的,原因是用户名不定长
03 //未知
0b //用户名长度
28 d2 44 2d 90 69 //本机MAC
c0 a8 c3 5f //本机IP
02 22 00 24 //这个是固定值
01 72 96 00 //这个来源于第一个回应包的第8到第11个字节
4a 6a 72 32 //校验值,具体方法下面会讲到
31 34 31 31 35 30 36 31 30 32 34 //用户名
6c 7a 79 2d 70 63 //主机名
df 05 05 05 //DNS-1
df 06 06 06 //DNS-2
...

判断字段固不固定的主要方法还是多次登录,多账号登录,然后判断哪些部分是一致的,哪些部分每次都不同。该数据包有四个字节4a 6a 72 32每次捕获到的都不一样,也猜不出是什么含义,故使用反汇编工具IDA Pro进行逆向工程,文件为DrAuthSvr.dll

首先找到一个开口:输入表里面的sendto函数。每次发送报文肯定都需要经过sendto函数,然后再通过交叉参考,最后找到发送数据的函数。我将其命名为send_udp_packet(char *buf,int len)

1
2
3
4
5
6
int __cdecl send_udp_packet(char *buf, int len)
{
...
if ( sendto(dword_1057E9C4, buf, len, 0, &to, 16) < 0 )
...
}

再次通过交叉参考,可以得知所有调用该函数的语句。其中有几个调用处写着:

1
send_udp_packet(buf, *(unsigned __int16 *)&buf[2])

这与该数据包正好吻合(长度为buf[2])。该数据包还有一个特征点即是buf[4]为3,最终可以确定发送这个包的函数。我们需要找的四个字节是[24:28],函数比较靠前的位置有:

1
2
*(_DWORD *)&buf[24] = 20000711;
*(_DWORD *)&buf[28] = 126;

也即是说函数直接对buf[24:32]的进行赋值。在比较靠后的位置还发现了:

1
2
3
4
5
6
7
8
9
v4 = 4 * ((*(unsigned __int16 *)&buf[2] + 3) / 4);
*(_WORD *)&buf[2] = v4;
v5 = (unsigned int)v4 >> 2;
v6 = 0;
for ( i = 0; i < (signed int)v5; ++i )
v6 ^= *(_DWORD *)&buf[4 * i];
*(_DWORD *)&buf[24] = 19680126 * v6;
*(_DWORD *)&buf[28] = 0;
dword_1057F5BC = 19680126 * v6;

也就是说变量v5为长度的1/4,然后做下面的运算,最后把19680126*v6回填到buf[24:28]中,并将buf[28]置零。需要注意的是这里采用的都是小端法。最后还将计算的值保存起来,后面会用到。Go语言实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 信息包校验码计算 */
func putCode1(buf []byte) {
v5 := len(buf) >> 2
var v6 uint32
var tmp uint32
var b_tmp *bytes.Buffer
for i := 0; i < v5; i++ {
b_tmp = bytes.NewBuffer(buf[4*i : 4*i+4])
binary.Read(b_tmp, binary.LittleEndian, &tmp)
v6 ^= tmp
}
binary.LittleEndian.PutUint32(buf[24:28], v6*19680126)
fmt.Println(v6 * 19680126)
binary.LittleEndian.PutUint32(globalCheck[:], v6*19680126)
}

将这个二百余个字节的信息包发送出去后,服务器会发出第二个响应包。

1
2
3
0000 07 01 30 00 04 0b 20 00 92 9a 9c 8c 01 00 00 00 ..0... .........
0010 44 39 d8 ed 0c 45 fd 03 af 85 30 15 3c fa 04 68 D9...E....0.<..h
0020 b0 82 00 60 00 00 00 00 00 00 00 00 00 00 00 00 ...`............

按照捕获到的数据包,客户端开始每隔一定时间发送两种不同的心跳包。

第一种心跳包有40个字节,正常情况下每次都会接连出现四个包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Ping-1
0000 07 02 28 00 0b 01 dc 02 6c 6f 00 00 00 00 00 00 ..(.....lo......
0010 4d 71 96 00 00 00 00 00 00 00 00 00 00 00 00 00 Mq..............
0020 00 00 00 00 00 00 00 00 ........
//Ping-2
0000 07 02 28 00 0b 02 dc 02 6c 6f 00 00 00 00 00 00 ..(.....lo......
0010 10 72 96 00 00 00 00 00 00 00 00 00 00 00 00 00 .r..............
0020 00 00 00 00 00 00 00 00 ........
//Ping-3
0000 07 03 28 00 0b 03 dc 02 6c 6f 00 00 00 00 00 00 ..(.....lo......
0010 10 72 96 00 00 00 00 00 37 87 84 02 c0 a8 c3 5f .r......7......_
0020 00 00 00 00 00 00 00 00 ........
//Ping-4
0000 07 03 28 00 0b 04 dc 02 6c 6f 00 00 00 00 00 00 ..(.....lo......
0010 11 72 96 00 00 00 00 00 00 00 00 00 00 00 00 00 .r..............
0020 00 00 00 00 00 00 00 00 ........

这四个数据包中,第1个和第3个为客户端发送,第2个和第4个则为服务器端响应。以最复杂的Ping-3作为例子进行分析:

1
2
3
4
5
6
7
8
9
07 //头
03 //计数器,一来一回为一次
00 28 //包长度,即40个字节
0b //固定
03 //步骤编号,该包为第三步
dc 02 6c 6f //不清楚,不影响登陆
10 72 96 //置零不影响
37 87 84 02 //校验值,具体方法下面会讲到
c0 a8 c3 5f //客户端IP

跟上面分析方法相同,可以发现37 87 84 02基本没什么规律,应该也是一种校验值。继续使用IDA Pro进行静态分析,可以找到一个函数带有语句buf[2]=40,即是我们要找的发包函数。手动将buf类型改为char[40]后,可以找到这样一个代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( buf[5] == 3 )
{
v5 = 0;
v14 = 0;
v6 = 0;
do
{
v7 = *(_WORD *)&buf[2 * v6++];
v5 ^= v7;
}
while ( v6 < 20 );
v8 = (unsigned int)(dword_10077544 + 1) < 3;
v9 = dword_10077544 == 2;
*(_DWORD *)&buf[24] = 711 * v5;

Go语言实现如下:

1
2
3
4
5
6
7
8
9
10
11
/* 40字节心跳包校验码计算 */
func putCode2(buf []byte) {
var tmp, v5 uint16
var b_tmp *bytes.Buffer
for i := 0; i < 20; i++ {
b_tmp = bytes.NewBuffer(buf[2*i : 2*i+2])
binary.Read(b_tmp, binary.LittleEndian, &tmp)
v5 ^= tmp
}
binary.LittleEndian.PutUint32(buf[24:28], uint32(v5)*711)
}

第二种心跳包是38个字节,在第一种心跳包发出后大约10秒发出。

1
2
3
0000 ff 4a 6a 72 32 45 c7 d8 6d 18 38 24 c0 20 91 2b .Jjr2E..m.8$. .+
0010 6c 00 00 00 44 72 63 6f c0 a8 7f 81 af 0b c0 a8 l...Drco........
0020 c3 5f 01 34 33 9b ._.43.

对这个数据包做分析:

1
2
3
4
5
6
7
8
9
ff //固定头
4a 6a 72 32 //发送信息包时的校验值
45 c7 d8 6d 18 38 24 c0 20 91 2b 6c //Challenged-Password的后12位
44 72 63 6f //字符串Drco
c0 a8 7f 81 //服务器IP
af 0b //下面会讲到
c0 a8 c3 5f //客户端IP
01 34 //下面会讲到
3e 9b //取当前时间的最后两个字节

第1个字节开始往后16个字节,通过与早前的数据包做比对即可推出;而最后的36、37个字节,则是通过IDA Pro得知:

1
2
3
v0 = _time64(0);
...
*(_WORD *)&buf[36] = v0;

而服务器IP和客户端IP后面分别带的两个字节,却始终未能使用逆向工程分析出来,Github上的代码也只是与反汇编出来的代码一致,跟我拿到的数据并不相符,最后只能通过慢慢比对,寻找线索。
首先可以发现的是,第34个字节固定为01,然后比对其他历史数据包,最终在服务器第二次响应的数据包中,找到了每次登陆都不同的三个字节:

1
2
3
0000 07 01 30 00 04 0b 20 00 92 9a 9c 8c 01 00 00 00 ..0... .........
0010 44 39 d8 ed 0c 45 fd 03 af 85 30 15 3c fa 04 68 D9...E....0.<..h
0020 b0 82 00 60 00 00 00 00 00 00 00 00 00 00 00 00 ...`............

其中第24、25个字节af 85,第31个字节68每次登陆都会有所变化,而第24个字节总是与上述心跳包第28个字节相同,第25个字节和心跳包第29个字节则存在以下关系:
$$
H_{29}=\left\{
\begin{array}{rcl}
R_{25}{\scriptstyle<<}1&&{R_{25}<128}\\
(R_{25}{\scriptstyle<<}1)|1&&{R_{25}\geq128}
\end{array} \right.
$$

同理可以得知第30个字节总是与心跳包第35个字节存在以下关系:

$$
H_{35}=\left\{
\begin{array}{rcl}
R_{30} {\scriptstyle >>}1&&{R_{30}为偶数}\\
(R_{30} {\scriptstyle >>}1)|128&&{R_{30}为奇数}
\end{array} \right.
$$

0x05 心跳包部分的Go语言实现

1
2
3
4
5
6
7
8
9
10
11
12
udpServerAddr, err = net.ResolveUDPAddr("udp4", "192.168.127.129:61440")
if err != nil {
fmt.Println(err)
os.Exit(0)
}
udpConn, err = net.DialUDP("udp4", nil, udpServerAddr)
if err != nil {
fmt.Println(err)
os.Exit(0)
}
defer udpConn.Close()
go recvPing()

用内置的net.DialUDP建立一个UDP对话到 192.168.127.129:61440,这里注意客户端的端口并不要求一定是61440,前面提到的那个关于UDP61440的错误,就是因为偶尔启动时61440端口由于各种原因被占用,其实只需要让系统自动分配一个端口即可。

然后启动一个协程,负责UDP回应的接受,对各种形式的回应做出处理。

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
/* 接收服务器的UDP回应 */
func recvPing() {
data := [4096]byte{}
for {
n, _, err := udpConn.ReadFromUDP(data[0:])
if err != nil {
fmt.Println(err)
}
if n > 0 {
if data[0] == 0x07 { //应答包
if data[2] == 0x10 && n == 32 { //第一次应答
sendPingInfo(data[8:12]) //发送用户信息包
} else if data[2] == 0x30 { //第二次应答
UknCode_1 = data[24]
UknCode_2 = data[25]
UknCode_3 = data[30]
UknCode_3 = data[31]
go pingCycle() //发送Ping-1
} else if data[2] == 0x28 { //Ping应答
if data[5] == 0x02 { //收到Ping-2
sendPing40(3) //发送Ping-3
}
}
}
}
}
}

还有一个协程则负责两种心跳包的交替发送。这里注意发送心跳包是定时发送的,因为并不是每个心跳包都会收到回应,不能依赖于收到回应后再继续发送。

1
2
3
4
5
6
7
8
9
10
/* 两种心跳包循环发送 */
func pingCycle() {
time.Sleep(1 * time.Second)
for {
sendPing40(1)
time.Sleep(10 * time.Second)
sendPing38()
time.Sleep(5 * time.Second)
}
}

其他包的发送函数就只是简单的对数据进行封装,这里就不再一一给出。

0x06 总结