前言
本文是对xingrz
写的针对安朗(安腾)宽带认证客户端
(以下统称蝴蝶)的协议分析记录的试译和修整,用以我编写第三方客户端。所有修改和翻译都在尊重原创的基础下进行,并且所有修改和翻译内容均属我个人意见与见解,如有错误,敬请谅解。
Swiftz
Swiftz
是一个基于对蝴蝶 3.6.5
的分析,旨在创建蝴蝶的便携式替代品的项目。该项目仅供研究,禁止黑客活动或任何其他非法目的。
版本
原始分析基于蝴蝶3.6.5
。
经测试与蝴蝶3.X.X~4.X.X
都兼容。
不兼容采用WEB
和PPPOE
认证方式的蝴蝶。
实现
- 支持Mac平台的蝴蝶
- 支持Linux平台的蝴蝶
- 支持Android/IOS平台的蝴蝶
- 支持Windows平台的蝴蝶
说明
本文中的协议是作为记录过程编写的。
例如:
local:3848 -> server:3848 crypto3848
LOGIN:
MAC as data(16)
USERNAME as string
PASSWORD as string
IP as string
ENTRY as string
DHCP as boolean
VERSION as string
- 从本地端口
3848
发送到远程服务器端口3848
(所有数据包通过UDP
协议发送)。 - 数据包用
crypto3848
加密。 - 数据包表示一个
LOGIN
行为(参阅下面的Consts
/Actions
部分)。 - 数据包包含这些字段(参阅下面的
Consts
/Fields
部分):MAC
:二进制数据,固定长度为16个字节USERNAME
:stringPASSWORD
:stringIP
:stringENTRY
:stringDHCP
:booleanVERSION
:string
分析
字符集
协议中的所有字符串均采用GB2312编码。
类型
- String:
GB2312
编码 - Char:1个字节(i.e.
0xFF
for255
) - Integer:4个字节(i.e.
0x00 0x00 0x00 0xFF
for255
) - Boolean:
0x00
forfalse
,0x01
fortrue
- Data:(i.e.
0x4A 0x21 0x39 0xC0
for4A2139C0
)
数据包结构
一个裸露的(未加密的)数据包在这个结构中:
- 1字节,表示这个数据包的作用
- 1字节,表示整个数据包的长度
- 16字节,
MD5
散列 - 1字节,第一个字段的
key
- 1字节,第一个字段的长度(包括
key
和key
的长度) - 第一个字段的数据
- 1字节,第二个字段的key(包括
key
和key
的长度) - 1字节 ,第二个字段的长度
- 第二个字段的数据
- ......
请注意,字段的长度比字段短2个字节。
构建数据包
- 按照上面的描述构建一个数据包,当然,这16个字节的
MD5
散列是16个字节的0x00
- 生成整个数据包的
MD5
散列 - 用你得到的
Hash
值填充这16个字节 - 加密
解析数据包
- 解密
- 验证
MD5
(为了安全,可选)
加密/解密
3848端口
该算法是一个非常简单的加密算法,改变每一个字节的顺序从ABCDEFGH
到HDEFCBAG
。
以下是JavaScript
中的一段代码供参考:
function encrypt (buffer) {
for (var i = 0; i < buffer.length; i++) {
buffer[i] = (buffer[i] & 0x80) >> 6
| (buffer[i] & 0x40) >> 4
| (buffer[i] & 0x20) >> 2
| (buffer[i] & 0x10) << 2
| (buffer[i] & 0x08) << 2
| (buffer[i] & 0x04) << 2
| (buffer[i] & 0x02) >> 1
| (buffer[i] & 0x01) << 7
}
}
function decrypt (buffer) {
for (var i = 0; i < buffer.length; i++) {
buffer[i] = (buffer[i] & 0x80) >> 7
| (buffer[i] & 0x40) >> 2
| (buffer[i] & 0x20) >> 2
| (buffer[i] & 0x10) >> 2
| (buffer[i] & 0x08) << 2
| (buffer[i] & 0x04) << 4
| (buffer[i] & 0x02) << 6
| (buffer[i] & 0x01) << 1
}
}
3849端口
类似3848
端口的算法,但不同的顺序,从ABCDEFGH
到ECBHAFDG
。
function encrypt (buffer) {
for (var i = 0; i < buffer.length; i++) {
buffer[i] = (buffer[i] & 0x80) >> 4
| (buffer[i] & 0x40) >> 1
| (buffer[i] & 0x20) << 1
| (buffer[i] & 0x10) >> 3
| (buffer[i] & 0x08) << 4
| (buffer[i] & 0x04)
| (buffer[i] & 0x02) >> 1
| (buffer[i] & 0x01) << 4
}
}
function decrypt (buffer) {
for (var i = 0; i < buffer.length; i++) {
buffer[i] = (buffer[i] & 0x80) >> 4
| (buffer[i] & 0x40) >> 1
| (buffer[i] & 0x20) << 1
| (buffer[i] & 0x10) >> 4
| (buffer[i] & 0x08) << 4
| (buffer[i] & 0x04)
| (buffer[i] & 0x02) << 3
| (buffer[i] & 0x01) << 1
}
}
流程
初始化
只需发送一个初始化数据包给1.1.1.8
而不必等待任何响应。
搜索
- 获取授权服务器
IP
- 获取
ENTRY
上线
- 发出在线请求,并等待回复。
- 如果成功,某些服务器可能还需要验证。
保持在线
- 每30秒向服务器发送一个
呼吸包
- 另外,需要注意可能收到的断线请求通知。
下线
发出下线请求,结束会话。
详解
初始化
只需通过UDP
发送这个纯文本到1.1.1.8
端口3850
。
info sock ini
- 不需要解密。
- 服务器没有响应。
搜索服务器
发送
local:3848 -> 1.1.1.8:3850 crypto3848
SERVER:
SESSION as string
IP as string(16)
MAC as data(16)
接收
1.1.1.8:3850 -> local:3848 crypto3848
SERVER_RET:
SERVER as data(4)
UNKNOWN0D as data(4)
在接收到的数据包当中,SERVER
的每个字节表示一个IPv4地址,例如在一个部分的4字节数据AC1001B4
就是172.16.1.180
。
搜索服务类型
发送
local:3848 -> server:3848 crypto3848
ENTRIES:
SESSION as string
MAC as binary(16)
接收
server:3848 -> local:3848 crypto3848
ENTRIES_RET:
ENTRY as string
ENTRY as string
...
- 可能不止一种类型。
上线
发送
local:3848 -> server:3848 crypto3848
LOGIN:
MAC as data(6)
USERNAME as string
PASSWORD as string
IP as string
ENTRY as string
DHCP as boolean
VERSION as string
接收
server:3848 -> local:3848 crypto3848
LOGIN_RET:
SUCCESS as boolean
SESSION as string
UNKNOWN05 as char
UNKNOWN06 as char
MESSAGE as string
UNKNOWN95 as data(24)
UNKNOWN06 as char
BLOCK34 as data(4)
BLOCK35 as data(4)
BLOCK36 as data(4)
BLOCK37 as data(4)
BLOCK38 as data(4)
WEBSITE as string
UNKNOWN23 as char
UNKNOWN20 as char
- 如果没有上线成功(例如密码错误),数据包只包含
SUCCESS
,SESSION
和MESSAGE
。 - 如果处于低速模式,将包含24字节的
0x00
和两个未知字段UNKNOWN95
,UNKNOWN06
。 - 接收的
SESSION
字段长度比字段短2字节。 - 接收的
MESSAGE
字段长度比字段短2字节。
验证
发送
local:random -> server:3849 crypto3849
CONFIRM:
USERNAME as string
MAC as data(6)
IP as string
ENTRY as string
接收
server:3849 -> local:random crypto3849
CONFIRM_RET
UNKNOWN30 as data(4)
UNKNOWN31 as data(4)
UNKNOWN32 as char
- 数据包通过相同的随机端口发送和接收。
0x00
可能是数据包进行MD5
校验之前出现的bug。整个数据包的长度是用这3个字节计算的。
保持在线
发送
local:3848 -> server:3848 crypto3848
BREATHE:
SESSION as string
IP as string(16)
MAC as data(16)
INDEX as integer
BLOCK2A as data(4)
BLOCK2B as data(4)
BLOCK2C as data(4)
BLOCK2D as data(4)
BLOCK2E as data(4)
BLOCK2F as data(4)
- 每发送一次呼吸包,
INDEX
的初始值0x01000000增加3。 - 除非
BREATHE_RET
返回是有效的,否则INDEX
不增加,因为在低速模式下可能会丢失数据包。
接收
server:3848 -> local:3848 crypto3848
BREATHE_RET:
SUCCESS as boolean
SESSION as string
INDEX as integer
- 字段
SESSION
的长度应比该字段短2个字节。
下线
发送
local:3848 -> server:3848 crypto3848
LOGOUT:
SESSION as string
IP as string(16)
MAC as data(16)
INDEX as integer
BLOCK2A as data(4)
BLOCK2B as data(4)
BLOCK2C as data(4)
BLOCK2D as data(4)
BLOCK2E as data(4)
BLOCK2F as data(4)
- 类似于呼吸包
接收
server:3848 -> local:3848 crypto3848
LOGOUT_RET:
SUCCESS as boolean
SESSION as string
INDEX as integer
- 字段
SESSION
的长度应比该字段短2个字节。
断开连接
一旦断开连接,你会收到来自服务器的数据包:
server:2001 -> local:4999 crypto3848
DISCONNECT:
SESSION as string
REASON as char
可能的原因:
0x00 - 长时间没有有效的呼吸包。
0x01 - 你被强行断开(可能是管理员)。
0x02 - 已达到网络流量限制。
- 字段
SESSION
的长度应比该字段短2个字节。
Actions/Fields
Actions
// 上线
LOGIN = 0x01
// 上线返回结果
LOGIN_RET = 0x02
// 呼吸
BREATHE = 0x03
// 呼吸返回结果
BREATHE_RET = 0x04
// 下线
LOGOUT = 0x05
// 下线返回结果
LOGOUT_RET = 0x06
// 搜索服务类型
ENTRIES = 0x07
// 搜索服务类型返回结果
ENTRIES_RET = 0x08
// 连接中断
DISCONNECT = 0x09
// 上线确认
CONFIRM = 0x0A
// 上线确认返回结果
CONFIRM_RET = 0x0B
// 搜索服务器
SERVER = 0X0C
// 搜索服务器返回结果
SERVER_RET = 0x0D
Fields
// 帐号
USERNAME = 0x01
// 密码
PASSWORD = 0x02
// 上线/呼吸/下线是否成功返回结果
SUCCESS = 0x03
// 在上线成功时出现的未知字节
UNKNOWN05 = 0x05
UNKNOWN06 = 0x06
// MAC物理地址
MAC = 0x07
// session (NOTE: wrong in return packet)
SESSION = 0x08
// IP地址
IP = 0x09
// 服务类型
ENTRY = 0x0A
// 上线错误返回的消息提示
MESSAGE = 0x0B
// 服务器IP地址
SERVER = 0x0C
// 在搜索服务器成功时出现的未知字节
UNKNOWN0D = 0x0D
// 启用DHCP
DHCP = 0x0E
// 自助服务网址
WEBSITE = 0x13
// serial no
INDEX = 0x14
// 版本号
VERSION = 0x1F
// 在上线成功时出现的未知字节
UNKNOWN20 = 0x20
UNKNOWN23 = 0x23
// 断开连接的原因
REASON = 0x24
// 4 bytes blocks, send in breathe and logout
BLOCK2A = 0x2A
BLOCK2B = 0x2B
BLOCK2C = 0x2C
BLOCK2D = 0x2D
BLOCK2E = 0x2E
BLOCK2F = 0x2F
// unknown 4 bytes blocks, appears while confirmed
BLOCK30 = 0x30
BLOCK31 = 0x31
// unknown
UNKOWN32 = 0x32
// 4 bytes blocks, appears while login successfully
BLOCK34 = 0x34
BLOCK35 = 0x35
BLOCK36 = 0x36
BLOCK37 = 0x37
BLOCK38 = 0x38
原版地址(英文):
https://github.com/xingrz/swiftz-protocal