与校园网斗智斗勇(二)之Swiftz协议试译

发布时间 2023-08-06 19:30:11作者: 一克猫

前言

  本文是对xingrz写的针对安朗(安腾)宽带认证客户端(以下统称蝴蝶)的协议分析记录的试译和修整,用以我编写第三方客户端。所有修改和翻译都在尊重原创的基础下进行,并且所有修改和翻译内容均属我个人意见与见解,如有错误,敬请谅解。

Swiftz

  Swiftz是一个基于对蝴蝶 3.6.5的分析,旨在创建蝴蝶的便携式替代品的项目。该项目仅供研究,禁止黑客活动或任何其他非法目的。

版本

原始分析基于蝴蝶3.6.5

经测试与蝴蝶3.X.X~4.X.X都兼容。

不兼容采用WEBPPPOE认证方式的蝴蝶。

实现

  • 支持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
  1. 从本地端口3848发送到远程服务器端口3848(所有数据包通过UDP协议发送)。
  2. 数据包用crypto3848加密。
  3. 数据包表示一个LOGIN行为(参阅下面的Consts / Actions部分)。
  4. 数据包包含这些字段(参阅下面的Consts / Fields部分):
    MAC:二进制数据,固定长度为16个字节
    USERNAME:string
    PASSWORD:string
    IP:string
    ENTRY:string
    DHCP:boolean
    VERSION:string

分析

字符集

协议中的所有字符串均采用GB2312编码。

类型

  • String:GB2312编码
  • Char:1个字节(i.e. 0xFF for 255
  • Integer:4个字节(i.e. 0x00 0x00 0x00 0xFF for 255
  • Boolean:0x00 for false0x01 for true
  • Data:(i.e. 0x4A 0x21 0x39 0xC0 for 4A2139C0

数据包结构

一个裸露的(未加密的)数据包在这个结构中:

  1. 1字节,表示这个数据包的作用
  2. 1字节,表示整个数据包的长度
  3. 16字节,MD5散列
  4. 1字节,第一个字段的key
  5. 1字节,第一个字段的长度(包括keykey的长度)
  6. 第一个字段的数据
  7. 1字节,第二个字段的key(包括keykey的长度)
  8. 1字节 ,第二个字段的长度
  9. 第二个字段的数据
  10. ......
    请注意,字段的长度比字段短2个字节。

构建数据包

  1. 按照上面的描述构建一个数据包,当然,这16个字节的MD5散列是16个字节的0x00
  2. 生成整个数据包的MD5散列
  3. 用你得到的Hash值填充这16个字节
  4. 加密

解析数据包

  1. 解密
  2. 验证MD5(为了安全,可选)

加密/解密

3848端口

该算法是一个非常简单的加密算法,改变每一个字节的顺序从ABCDEFGHHDEFCBAG
以下是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端口的算法,但不同的顺序,从ABCDEFGHECBHAFDG

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而不必等待任何响应。

搜索

  1. 获取授权服务器IP
  2. 获取ENTRY

上线

  1. 发出在线请求,并等待回复。
  2. 如果成功,某些服务器可能还需要验证。

保持在线

  1. 每30秒向服务器发送一个呼吸包
  2. 另外,需要注意可能收到的断线请求通知。

下线

发出下线请求,结束会话。

详解

初始化

只需通过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
  • 如果没有上线成功(例如密码错误),数据包只包含SUCCESSSESSIONMESSAGE
  • 如果处于低速模式,将包含24字节的0x00和两个未知字段UNKNOWN95UNKNOWN06
  • 接收的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