DrCom 5.2.1(X) 版协议分析 —— UDP 协议
前面说了那么多,估计已经一脸懵逼了,没关系,现在才是重头戏,也是认证协议的重中之重,就是复杂到反人类的 UDP 协议╮(╯_╰)╭。
现在来讲讲 UDP 认证的作用,其实具体什么作用我也不知道,在协议分析的过程中,如果不按套路来进行 UDP 认证和心跳包的发送,最长上线时间仅能维持21分钟,随后就会被服务器强制下线,所以我推测 UDP 只是起到了心跳维持状态的用处。
问题的发现
在完成了 EAP 协议的分析之后,我们使用 C++ 实现了 EAP 协议的认证过程,测试过程中使用 Wireshark 抓包的过程中,发现了一个严重的问题,在历经大约21分钟的上线之后,毫无例外的都会收到服务器发来的 Failure 包,这意味着我们被服务器强制下线了。
Hangzhou_56:78:00 Clevo_18:5f:14 EAP 60 Failure
0000 80 fa 6c 18 5f 14 58 6a b1 56 78 00 88 8e 01 00 ..[._.Xj.Vx.....
0010 00 07 04 02 00 07 08 01 05 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 95 3e b9 e8 .........>..
不可否认的是,这个问题严重影响了使用,我们再一次对官方拨号器认证过程进行细致的分析,抓包文件的数据量很多,从中筛选出有用的信息至关重要。偶然一次机会,我们注意到客户端会时不时报以下的错误:
获取用户属性超时!请检查防火墙配置允许 UDP 61440 端口。
因此我们在过滤器中设置下列过滤条件:udp.port == 61440
很快发现,客户端在 EAP 协议认证成功之后,会持续向特定的认证服务器(192.168.127.129:61440)发送和接收 UDP 数据包。
协议的分析
(以下出现的报文数据仅为 UDP 报文的数据部分)
在发现了上述问题之后,便开始对 UDP 数据包进行分析,在完成 EAP 认证流程之后,客户端会自动向服务器发送 UDP 报文,一共有8个字节,我们将其记为 U8,具体含义未知,每次发送数据包均相同:
192.168.196.65 192.168.127.129 UDP 50 61440->61440 Len=8
0000 07 00 08 00 01 00 00 00 ........
随后,服务器方面会针对这个数据包回应一个报文:
192.168.127.129 192.168.196.65 UDP 74 61440->61440 Len=32
0000 07 00 10 00 02 00 00 00 3f 23 1d 02 c0 a8 c4 41 ........?#.....A
0010 a8 ac 00 00 4f e4 16 c1 00 00 00 00 dc 02 00 00 ....O...........
在收到这个服务器返回的报文后,客户端马上又向服务器发送了另一个 UDP 报文,我们将其记为 U244:
192.168.196.65 192.168.127.129 UDP 286 61440->61440 Len=244
0000 07 01 f4 00 03 0b 80 fa 6c 18 5f 14 c0 a8 c4 41 ........[._....A
0010 02 22 00 26 3f 23 1d 02 f0 a6 25 bd 00 00 00 00 .".&?#....%.....
0020 31 31 31 31 31 31 31 31 31 31 31 57 5a 4a 00 00 11111111111WZJ..
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 ff ff ff ff 00 .........
.......
0050 00 00 00 00 00 00 00 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 00 b8 01 26 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 39 31 39 31 36 31 63 33 64 61 62 34 33 ...919161c3dab43
00c0 35 32 31 35 64 63 30 31 33 30 38 35 65 39 35 32 5215dc013085e952
00d0 66 64 62 63 36 66 35 62 65 36 36 00 00 00 00 00 fdbc6f5be66.....
00e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00f0 00 00 00 00 ....
具体分析如下:
07 // 固定头
01 // 计数器,这里为固定值
f4 00 // 数据部分长度,不固定,取决于账号长度
03 // 固定
0b // 用户名长度
80 fa 6c 18 5f 14 // 本机 Mac 地址
c0 a8 c4 41 // 本机 IP 地址
02 22 00 26 // 未知,取决于软件版本
3f 23 1d 02 // 来自 U8 的返回报文数据部分的 8-11 位
f0 a6 25 bd // U244 报文校验值,算法将在后面介绍,需要保存下来
00 00 00 00 // 固定
31 35 31 31 31 31 31 31 31 31 31 // 内网认证账号,这里为学号,为了隐私已进行打码
57 5a 4a 00 00 00 00 00 00 00 00 00 00 00 00 // 主机名
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 // 固定
ff ff ff ff // DNS1,可置空
00 00 00 00 // 固定
00 00 00 00 // DNS2,可置空
00 00 00 00 00 00 00 00 94 00 00 00 06 00 00 00 02 00 00 00 f0 23 00 00 02 00 00 00 // 固定
44 72 43 4f 4d // 固定字符,DrCOM
00 b8 01 26 // 推测为软件版本
39 31 39 31 36 31 63 33 64 61 62 34 33 35 32 31 35 64 63 30 31 33 30 38 35 65 39 35 32 66 64 62 63 36 66 35 62 65 36 36 // 客户端验证模块(DrAuthSvr.dll)的 40 位 MD5 哈希值
(中间有大段置空数据为固定,在此被忽略)
其中,客户端验证模块(DrAuthSvr.dll)的 40 位 MD5 哈希值是从客户端的安装路径中的 Log/auth_log.txt 日志文件中发现的:
Dr.COM Protocol Ver: 0.8(U62.R110908)(1x)Build(k38.20150921)
AuthModuleFileName: C:\Drcom\DrUpdateClient\drauthsvr.dll
AuthModuleFileHash: 919161c3dab435215dc013085e952fdbc6f5be66(40)
AuthModuleFileHash 为验证模块文件的 Hash 值,即 DrAuthSvr.dll 文件,但通过 MD5 文件校验工具只能算出前32位,后8位无法得知算法。
上面的分析中有提到一个四字节的校验值,这四个字节(f0 a6 25 bd
)每次捕获到的都不一样,也猜不出是什么含义,故使用反汇编工具 IDA Pro 对验证模块进行逆向工程分析。
经过分析,我们在 IDA Pro 中的 Imports 表中发现了一个数据包发送的函数:
int __stdcall sendto( SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen);
随后我们使用 IDA Pro 的搜索功能,查找出了调用了上述发送函数的所有函数,经过交叉参考,我们发现发送数据的函数:
int __cdecl sub_10038800(char *buf, int len){
...
if (sendto (dword_1057E9C4), buf, len, 0, &stru_1057E9CC, 16) < 0)
...
}
发现这个函数之后,随即用 IDA Pro 查询出调用该函数的函数,通过交叉参考,发现了其中一个函数:
int sub_10001240() {
...
return sub_10038800( buf, *(unsigned __int16* )&buf[2] );
}
看到这里,好像走上了正轨,但是发现有些参数好像不对,buf 的长度才声明为 4,随即便使用 IDA Pro 修改了 buf 的声明长度为帧的数据长度 244,得到:buf[0] = 7; buf[4] = 3;
由此确定了这个函数就是我们要找的。随后,在靠前的部分发现了:
*(__DWORD *) &buf[24] = 20000711;
*(__DWORD *) &buf[28] = 126;
并在靠后的位置发现了:
v4 = 4 * ((*(unsigned __int16 *)&buf[2] + 3) / 4);
*(_WORD *)&buf[2] = v4;
v5 = (unsigned int)v4 >> 2;
v6 = 0;
for ( i = 0; i < v5; ++i )
v6 ^= *(_DWORD *)&buf[4 * i];
*(__DWORD *) &buf[24] = 19680126 * v6;
*(__DWORD *) &buf[28] = 0;
dword_1057F5BC = 19680126 * v6;
dword_1057EBCC = 5;
*(_DWORD *)dword_1057E9C0 = 703;
sub_100292F0(aDrcom8021xEx_0);
dword_10578ED0 = GetTickCount();
return sub_10038800(buf, *(unsigned __int16 *)&buf[2]);
也就是说,将经过一系列计算的 v6 回填到了 buf24:28 中,还将 buf28 置0,还有一点是,需要把计算后的值保留下来,接下来还会使用。C++ 实现如下,在接下来构造数据包的时候还会涉及以下的函数:
// U244 校验值算法
uint32_t func(uint8_t buf[]) {
uint32_t drcom_protocol_param = 20000711;
memcpy(&buf[24], &drcom_protocol_param, 4);
buf[28] = 126;
uint16_t len = buf[2];
uint16_t t = len >> 2;
uint32_t tmp = 0;
uint32_t v6 = 0;
for (int i = 0; i < t; i++) {
memcpy(&tmp, &buf[4*i], 4);
v6 ^= tmp;
}
buf[28] = 0;
return v6*19680126;
}
在完成这个两百多字节的 UDP 报文的发送之后,服务器连续返回了两个 UDP 报文:
192.168.127.129 192.168.196.65 UDP 90 61440->61440 Len=48
0000 07 01 30 00 04 0b 20 00 bc 69 49 2f 01 00 00 00 ..0... ..iI/....
0010 44 39 d8 ed 0c 45 fd 03 6b 0a 30 15 5c 0a 04 59 D9...E..k.0.\..Y
0020 b0 82 00 60 00 00 00 00 00 00 00 00 00 00 00 00 ...`............
192.168.127.129 192.168.196.65 UDP 138 61440->61440 Len=96
0000 4d 38 24 ae cd f8 c2 e7 b9 ca d5 cf b1 a8 d0 de M8$.............
0010 b7 bd ca bd a3 ba b5 c7 c2 bd 4d 49 53 cf b5 cd ..........MIS...
0020 b3 b1 a8 d0 de a3 a8 b5 e7 c4 d4 b5 c7 c2 bc 6d ...............m
0030 69 73 2e 73 67 75 2e 65 64 75 2e 63 6e a3 ac ca is.sgu.edu.cn...
0040 d6 bb fa b5 c7 c2 bc 6d 69 73 2e 73 67 75 2e 65 .......mis.sgu.e
0050 64 75 2e 63 6e 2f 70 68 6f 6e 65 a3 a9 a1 a3 00 du.cn/phone.....
(第二个包为返回的网关通知,不做详细介绍)
在接收到这两个返回包之后,客户端便开始周期性且有规律的循环发送 UDP 心跳报文。
心跳包
第一种:长为 40 字节的心跳包,我们记为 U40-1 包和 U40-2 包
一般情况下,会有连续的4个包出现
U40-1 Request:
192.168.196.65 192.168.127.129 UDP 82 61440->61440 Len=40
0000 07 01 28 00 0b 01 dc 02 70 7b 00 00 00 00 00 00 ..(.....p{......
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 ........
U40-1 Response:
192.168.127.129 192.168.196.65 UDP 82 61440->61440 Len=40
0000 07 01 28 00 0b 02 dc 02 70 7b 00 00 00 00 00 00 ..(.....p{......
0010 5c 23 1d 02 00 00 00 00 00 00 00 00 00 00 00 00 \#..............
0020 00 00 00 00 00 00 00 00 ........
U40-2 Request:
192.168.196.65 192.168.127.129 UDP 82 61440->61440 Len=40
0000 07 02 28 00 0b 03 dc 02 70 7b 00 00 00 00 00 00 ..(.....p{......
0010 5c 23 1d 02 00 00 00 00 94 06 eb 01 c0 a8 c4 41 \#.............A
0020 00 00 00 00 00 00 00 00 ........
U40-2 Response:
192.168.127.129 192.168.196.65 UDP 82 61440->61440 Len=40
0000 07 02 28 00 0b 04 dc 02 70 7b 00 00 00 00 00 00 ..(.....p{......
0010 5d 23 1d 02 00 00 00 00 00 00 00 00 00 00 00 00 ]#..............
0020 00 00 00 00 00 00 00 00 ........
这四个数据包中,第1个和第3个为客户端发送,第2个和第4个则为服务器端响应。
U40-1 Request 包分析:
07 // 固定头
01 // 计数器,完成一个来回计数器加一
28 00 // 包长,40 字节
0b // 固定
01 // 包类型
dc 02 // 固定
70 7b // 两位随机生成值,用于分辨同一组包
00 00 00 00 00 00 // 固定,置空
5c 23 1d 02 // 来自上个 U40-1 Response 包的 16-19 位
00 00 00 00 // 固定,置空
94 06 eb 01 // U40 校验值,算法在后面介绍
c0 a8 c4 41 // 客户端绑定的静态 IP 地址
00 00 00 00 00 00 00 00 // 固定,置空
最复杂的 U40-2 Request 包分析:
07 // 固定头
02 // 计数器,完成一个来回计数器加一
28 00 // 包长,40 字节
0b // 固定
03 // 包类型
dc 02 // 固定
70 7b // 两位随机生成值,用于分辨同一组包
00 00 00 00 00 00 // 固定,置空
5c 23 1d 02 // 来自上个 U40-1 Response 包的 16-19 位
00 00 00 00 // 固定,置空
94 06 eb 01 // U40 校验值,算法在后面介绍
c0 a8 c4 41 // 客户端绑定的静态 IP 地址
00 00 00 00 00 00 00 00 // 固定,置空
其中的两位随机生成值用于分辨同一组包,而这里的 U40-1 和 U40-2 包属于同一组。
和 U244 报文中的校验值一样,U40-2 Request 包中的 24-27 位每次发送都不一样,故猜测为该数据包的校验位,因此继续使用 IDA Pro 进行逆向分析,使用相同的方法,发现了以下代码段:
if ( buf[5] == 3 )
{
v5 = 0;
v13 = 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;
++dword_10077544;
...
}
C++实现如下:
// U40-2 校验值算法
uint32_t func(uint8_t buf[]) {
int16_t v7 = 0;
uint16_t v5 = 0;
for (int i = 0; i < 20; i++) {
memcpy(&v7, &buf[2*i], 2);
v5 ^= v7;
}
return uint32_t(v5)*711;
}
上述数据包中有部分随机生成的字节,故附上随机生成函数:
uint16_t xsrand(void) {
return ((((uint16_t)rand()<<4)&0xF0u)
| ((uint16_t)rand()&0x0Fu));
}
第二种:长为 38 字节的心跳包,我们记为 U38 包
一般情况下在上述的 U40 循环后10秒内发出,下面对这种包进行分析:
使用 IDA Pro 进行逆向分析,发现代码段:
证明该包最后两位为系统当前时间最后两位。
ff // 固定头
f0 a6 25 bd // U244 校验和,前面计算后需要保存下来
1a 68 39 fd 2f 19 55 d2 6a 7b 54 34 // 来自 EAP-MD5-Challenge 后12位
00 00 00 // 固定
44 72 63 6f // 固定字符串,Drco
c0 a8 7f 81 // 服务器 IP
6b // 来自 U244 发送后反馈包 25 位
14 // 经过 U244 发送后反馈包 26 位计算得来,算法下面会提到
c0 a8 c4 41 // 客户端 IP
01 // 固定
ac // 经过 U244 发送后反馈包 32 位计算得来,算法下面会提到
61 b9 // 系统当前时间最后两位(Unix时间戳)
其中第 29 位(14
),第 35 位(ac
)每次登陆都会有所变化,和 U244 发送后返回包的第 25 和 31 位的关系如下:
//第29位
int main()
{
uint8_t a = 0x02;
uint8_t tmp = a << 1;
if (a >= 128)
tmp |= 1;
printf("%02x\n", tmp);
return 0;
}
//第35位
int main()
{
uint8_t a = 0x85;
uint8_t tmp = a >> 1;
if (a % 2 != 0)
tmp |= 128;
printf("%02x\n", tmp);
return 0;
}
第三种:另一个长为 40 字节的心跳包,我们记为 U40-3 包
此外,我们还发现了一种容易被忽略的 U40 数据包,官方客户端有一定概率在上线后马上发送这种数据包,还会在上面 U40 数据包循环10次后发送一次,服务器会马上返回一个数据长度为 272 的报文,下面将简单介绍该包和其它 U40 包的区别(与 U40-1 对比):
- 上线之后马上发出:
- 计数器:00;
- 包类型后的固定位:db 02;
- 10次 U40 循环后发出:
- 包类型的固定位:db 02;
目前不清楚这种包的作用,发送后会马上开始新一轮的 U40 循环。
以上就是 DrCom 5.2.1(X) 版协议分析 —— UDP 协议的所有数据包的分析,至于包的发送,在进行对数据的打包后(这里不再赘述),就可以使用系统中内置的 Socket 库发送 UDP 报文到 192.168.127.129:61440,这里注意客户端的端口并不要求一定是 61440,前面提到的那个关于 UDP 61440 的错误,仅仅是因为官方客户端抽风指定了发送端口,如果系统其他服务占用了 61440 端口,导致官方客户端无法占用这个端口,就会登录失败,其实只要让系统自动分配一个端口来通信即可。
关于具体代码的实现,可以到 GitHub 上查看详细的源码。