MMS 协议

概述

MMS 提供了一套面向对象的客户端/服务器通信模型,核心功能包括:

  • 读/写变量:读取或修改设备内部的数据点(如温度、电压、开关状态)

  • 报告:设备在数据变化或事件触发时主动向客户端推送信息

  • 文件操作:上传/下载设备上的配置文件或固件

  • 控制:启停设备、执行预定义程序等。

  • 设备建模:通过虚拟制造设备(VMD)和域(Domain)等抽象概念,统一不同厂家的设备数据接口

最常见的应用场景是 电力系统 IEC 61850 通信(变电站自动化),以及制造业的 SCADA 系统、机器人控制等

报文结构

MMS 协议的 PDU 完全使用 ASN.1 基本编码规则(BER) 来描述,也就是 Tag - Length - Value 结构

BER 快速回顾

  • Tag(标签):1~多字节,说明数据类型及上下文

  • Length(长度):1~多字节,指明后续 Value 的字节数

  • Value(内容):可以是简单整数、字符串,也可以是嵌套的 TLV 结构

MMS 中几乎都用上下文特定的构造标签(如 0xA0、0xA1 等)

顶层的 MMS PDU 类型

MMS 顶层 PDU 使用 ASN.1 BER 编码中的 Context-specific 标签。 对于 [0]~[13] 这类低编号标签,其顶层 BER Tag 可直接对应为 A0~AD。

PDU 类型 ASN.1 标签 BER 顶层标签 说明
confirmed-RequestPDU [0] A0 确认请求,需要对端响应
confirmed-ResponsePDU [1] A1 对确认请求的正常响应
confirmed-ErrorPDU [2] A2 对确认请求返回的错误
unconfirmed-PDU [3] A3 无确认报文,如 InformationReport
rejectPDU [4] A4 拒绝某个 MMS PDU
cancel-RequestPDU [5] A5 取消请求
cancel-ResponsePDU [6] A6 取消响应
cancel-ErrorPDU [7] A7 取消操作错误
initiate-RequestPDU [8] A8 MMS 会话初始化请求
initiate-ResponsePDU [9] A9 MMS 会话初始化响应
initiate-ErrorPDU [10] AA 初始化过程错误
conclude-RequestPDU [11] AB 请求结束 MMS 会话
conclude-ResponsePDU [12] AC 对结束会话请求的响应
conclude-ErrorPDU [13] AD 结束会话过程错误

实战案例

窃取数据的黑客

查看会话可以看到一直在跟 192.168.51.34:102 通信

所以 192.168.51.34 就是服务器

按照长度排列,第 3096 包没有 info 信息有点可疑(3022、3059 也一样)

展开后可以看到 flag.7z 以及 flag.txt

按照最先发包顺序,从 3022 之后看攻击者 IP 192.168.51.33 请求操作

在 3066 包中可以看到访问了 flag.7z

编写脚本去响应中找到这个压缩文件

import struct

pcap = "tmflag(1).pcapng"               # 要解析的 pcapng 文件
data = open(pcap, "rb").read()           # 一次性读取全部二进制内容

endian = "<"                             # 默认小端字节序,后续可根据块类型修正
pos = 0                                  # 当前解析位置(字节偏移)
packets = []                             # 存储提取的原始数据包内容

# 逐块解析 pcapng 文件
while pos + 12 <= len(data):            # 至少需要类型(4) + 块长(4) + 尾部块长(4)
    block_type, block_len = struct.unpack(endian + "II", data[pos:pos+8])
    if block_len < 12 or pos + block_len > len(data):  # 非法块长度则停止解析
        break

    body = data[pos+8:pos+block_len-4]  # 块体,去掉头和尾的块长度字段

    if block_type == 0x0A0D0D0A:        # Section Header Block:决定字节序
        bom = struct.unpack("<I", body[:4])[0]  # 读取字节序标记
        endian = "<" if bom == 0x1A2B3C4D else ">"  # 根据标记设置端序
    elif block_type == 6:               # Enhanced Packet Block
        if len(body) >= 20:
            # 接口ID、时间戳高/低、捕获长度、原始长度(只关注 caplen)
            _, _, _, caplen, _ = struct.unpack(endian + "IIIII", body[:20])
            packets.append(body[20:20+caplen])  # 保存捕获到的数据包

    pos += block_len                     # 移动到下一个块

# 分析提取的以太网数据包
for idx, pkt in enumerate(packets, 1):
    if len(pkt) < 14:
        continue                         # 不够以太网帧头长度,跳过

    eth_type = struct.unpack("!H", pkt[12:14])[0]  # 以太网类型字段
    if eth_type != 0x0800:
        continue                         # 非 IPv4 包则跳过

    ip = pkt[14:]                        # IP 头部起始
    ihl = (ip[0] & 0x0f) * 4            # IP 头长度(字节)
    proto = ip[9]                        # 传输层协议号
    if proto != 6:
        continue                         # 非 TCP 则跳过

    total_len = struct.unpack("!H", ip[2:4])[0]  # IP 总长度
    tcp = ip[ihl:total_len]              # TCP 段(头部+数据)
    sport, dport = struct.unpack("!HH", tcp[:4])  # 源端口、目的端口
    tcp_hlen = (tcp[12] >> 4) * 4        # TCP 头长度(字节)
    payload = tcp[tcp_hlen:]             # TCP 有效载荷(应用层数据)

    # 7z 文件魔数签名
    sig = b"7z\xbc\xaf\x27\x1c"
    off = payload.find(sig)              # 在载荷中查找该签名

    if off != -1:                        # 找到 7z 文件头
        # 提取从签名开始的一段数据(长度 0x8f 字节)
        filedata = payload[off:off + 0x8f]
        open("flag.7z", "wb").write(filedata)  # 写出文件
        print(f"[+] frame {idx}: extracted flag.7z, size={len(filedata)} bytes")

解压后拿到 flag 值

评论