Intel AMT discovery

There's been some articles lately about Intel AMT and its impact on security, trust, privacy and free-software. AMT supposed to be widely deployed in newest Intel hardware. So I wanted to see if I could find some AMT devices in the wild.

Update: 2017-05-15 Add references related to CVE-2017-5689 (AMT vulerability).

What's AMT anyway?

AMT is an Intel technology for out of band management (without cooperation of the OS) of a computer over the network even if the computer is turned off. It can be used to do things such as:

  • booting, shutting down, rebooting, waking the computer;

  • changing the booting method (such as enabling PXE);

  • serial-over-LAN, KVM, IDE and USB redirections, etc.

It implementats the DASH standard and is similar to IPMI in terms of features. It uses SOAP over HTTP with some WS-* greatness and comes with bells and whistles such as integration with Active Directory.

When AMT is enabled, IP packets incoming on the builtin network adapter for some TCP and UDP ports are sent directly to the ME instead of reaching the OS. The ME has its own processor and its own OS and can give access to the hardware over the network. Usually, the ME and the main system share the same network interface, MAC address and IPv6 address.

Relevant citation for DASH:

A physical system’s out-of-band Management Access Point and the In-Band host shall share the MAC address and IPv4 address of the network interface. Manageability traffic shall be routed to the MAP through the well known system ports defined by IANA.

Relevant citation for AMT:

TCP/UDP messages addressed to certain registered ports are routed to Intel AMT when those ports are enabled. Messages received on a wired LAN interface go directly to Intel AMT. Messages received on a wireless interface go to the host wireless driver. The driver detects the destination port and sends the message to Intel AMT.

My machine

My work laptop has a MEI device and the system loads the MEI Linux module:

$ lspci
[...]
00:16.0 Communication controller: Intel Corporation 7 Series/C210 Series Chipset Family MEI Controller #1 (rev 04)
[...]

$ lsmod | grep mei
mei_me                 32768  0
mei                    94208  1 mei_me

$ ls -l /dev/mei*
crw------- 1 root root 246, 0 juil.  7 08:53 /dev/mei0

$ grep mei /lib/modules/4.6.0-1-amd64/modules.alias
alias pci:v00008086d00005A9Asv*sd*bc*sc*i* mei_me
alias pci:v00008086d00001A9Asv*sd*bc*sc*i* mei_me
[...]
alias mei:pn544:0bb17a78-2a8e-4c50-94d4-50266723775c:*:* pn544_me

$ cat /sys/bus/pci/drivers/mei_me/0000:00:16.0/uevent
MAJOR=248
MINOR=0
DEVNAME=mei0

MEI is a PCI-based interface to the ME from within the computer.

However, there is no option to disable AMT in the BIOS on my laptop. Apparently, AMT is not enabled on this device even if this not absolutely clear. The hardware seems to be there though.

AMT Discovery

We can use the discovery mechanism of AMT in order to detect AMT devices on a network. The AMT (and DASH) discovery uses two phases:

  1. the first phase uses ASF RMCP;

  2. the second phase uses the WS-Management Identify method.

The second phase is not so useful so I'll focus on the first one.

Implementation

The first phase is quite simple:

  1. the client sends a (possibly) broadcast RMCP Presence Ping message over UDP port 623 (asf-rmcp);

  2. the nodes supporting ASF (such as DASH/AMT and IPMI nodes) send a RMCP Presence Pong.

RMCP Header:

Size Field
1B Version (0x6 for RMCP 1.0)
1B Reserved
1B Sequence number (0--254, 255 when no no acknowledge is needed)
1B Class of Message
Bit 7, 1 for acknowledge
Bits 6:4, reserved
Bits 3:0, 6 for ASF, 7 for IPMI, etc.

All messages which are not acknowledges have a RMCP data field after the header:

Size Field
4B IANA Entreprise Number, servces as a namespace for the message type (4542 for ASF-RMCP)
1B Message Type (for ASF-RMCP, we have 0x80 for Presence Ping, 0x40 for Presence Pong)
1B Message Tag
1B Reserved
1B Data Length
Var Data (payload)

We can handle RMCP messages with:

ASF_RMCP_VERSION1 = 0x6
IANA_ASF = 4542
ASF_RMCP_FORMAT = "!BBBBIBBBB"

# RCMP ASF message (not ack)
class Message:
    def __init__(self):
        self.version = ASF_RMCP_VERSION1
        self.reserved = 0x00
        self.seqno = 0x00
        self.message_class = 0x00
        self.entreprise_number = IANA_ASF
        self.message_type = 0x00
        self.message_tag = 0x00
        self.reserved = 0x00
        self.data = bytearray()

    def load(self, message):
        if (len(message) < struct.calcsize(ASF_RMCP_FORMAT)):
            raise "Message too small"
        (self.version, self.reserved, self.seqno, self.message_class,
         self.entreprise_number, self.message_type, self.message_tag,
         self.reserved, data_length) = \
            struct.unpack_from(ASF_RMCP_FORMAT, message)
        if len(message) != data_length + struct.calcsize(ASF_RMCP_FORMAT):
            raise "Bad length"
        rmcp_size = struct.calcsize(ASF_RMCP_FORMAT)
        self.data = bytearray(memoryview(message)[rmcp_size:])

    def to_bytes(self):
        size = struct.calcsize(ASF_RMCP_FORMAT) + len(self.data)
        res = bytearray(size)
        struct.pack_into(ASF_RMCP_FORMAT, res, 0,
                         self.version, self.reserved, self.seqno,
                         self.message_class, self.entreprise_number,
                         self.message_type, self.message_tag, self.reserved,
                         len(self.data))
        memoryview(res)[struct.calcsize(ASF_RMCP_FORMAT):] = self.data
        return res

For Presence Ping, there is no payload. For Presence Pong, the payload is:

Size Field
4B IANA Entreprise Number (4542 if not OEM specific-things are used)
4B OEM Defined
1B Supported Entities
Bit 7, set if IPMI is supported
Bits 6:4, reserved
Bits 3:0, 1 for ASF version 1.0
1B Supported interactions
Bit 5: set if DASH (AMT) is supported
5B Reserved

We can handle Pong Presence data with:

ASF_RMCP_PONG_FORMAT = "!IIBBBBBBBB"

class PongData:
    def __init__(self, payload):
        if struct.calcsize(ASF_RMCP_PONG_FORMAT) != len(payload):
            print("Bad length for pong payload expected %i but was %i" %
                  (struct.calcsize(ASF_RMCP_PONG_FORMAT), len(payload)))
        (self.entreprise_number, self.oem_defined, self.supported_entities,
         self.supported_interactions, self.reserved1,
         self.reserved2, self.reserved3, self.reserved4, self.reserved5,
         self.reserved6) = struct.unpack_from(ASF_RMCP_PONG_FORMAT, payload)

    def ipmi(self):
        return (self.supported_entities & 127) != 0

    def asf(self):
        return (self.supported_entities & 15) == 1

    def dash(self):
        return (self.supported_interactions & 32) != 0

    def features(self):
        res = []
        if self.ipmi():
            res.append("ipmi")
        if self.asf():
            res.append("asf")
        if self.dash():
            res.append("dash")
        return res

We send a Presence Ping message to some (possibly broadcast) address:

ASF_RMCP_PORT = 623
ASF_RMCP_MESSAGE_TYPE_PRESENCE_PING = 0x80

m = Message()
m.message_class = ASF_RMCP_VERSION1
m.message_type = ASF_RMCP_MESSAGE_TYPE_PRESENCE_PING

sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(m.to_bytes(), (address, ASF_RMCP_PORT))

And then we process the messages:

ASF_RMCP_MESSAGE_TYPE_PRESENCE_PONG = 0x40
ASF_RMCP_MESSAGE_TYPE_PRESENCE_PING_ACK = 0x86

sock.settimeout(1)

try:
    while True:
        data, addr = sock.recvfrom(1024)
        logging.debug("From " + str(addr[0]) + ": " + str(data))
        if len(data) == 4 and data[0] == ASF_RMCP_VERSION1 and data[2] == 0 \
                and data[3] == ASF_RMCP_MESSAGE_TYPE_PRESENCE_PING_ACK:
            logging.debug("Ack from " + str(addr[0]))
            continue
        try:
            m.load(data)
        except:
            continue
        if m.message_type == ASF_RMCP_MESSAGE_TYPE_PRESENCE_PONG:
            # Pong:
            print(str(addr[0]))
            pongData = PongData(m.data)
            features = pongData.features()
            print("\tEntreprise: %s" %
                  entreprise_name(pongData.entreprise_number))
            if len(features) != 0:
                print("\tFeatures: %s" % ",".join(features))
except socket.timeout:
    pass

Full code

Here's the full code:

#!/usr/bin/env python3
# Use ASF RMCP to discover RMCP-aware nodes (such as AMT/AMT or IPMI)
# Keywords: DMTF, ASF RMCP, DASH, AMT, IPMI.

import socket
import ctypes
import struct
import sys
import logging
import ipaddress

ASF_RMCP_PORT = 623
ASF_RMCP_FORMAT = "!BBBBIBBBB"
ASF_RMCP_PONG_FORMAT = "!IIBBBBBBBB"
ASF_RMCP_VERSION1 = 0x6
ASF_RMCP_MESSAGE_TYPE_PRESENCE_PONG = 0x40
ASF_RMCP_MESSAGE_TYPE_PRESENCE_PING = 0x80
ASF_RMCP_MESSAGE_TYPE_PRESENCE_PING_ACK = 0x86
IANA_ASF = 4542

address = sys.argv[1]
if ipaddress.ip_address(address).version == 4:
    address = "::ffff:" + address

entreprise_names = {
    343: "Intel",
    3704: "AMD",
    4542: "Alerting Specifications Forum",
}


def entreprise_name(n):
    if n in entreprise_names:
        return entreprise_names[n]
    else:
        return str(n)


# RCMP ASF message (not ack)
class Message:
    def __init__(self):
        self.version = ASF_RMCP_VERSION1
        self.reserved = 0x00
        self.seqno = 0x00
        self.message_class = 0x00
        self.entreprise_number = IANA_ASF
        self.message_type = 0x00
        self.message_tag = 0x00
        self.reserved = 0x00
        self.data = bytearray()

    def load(self, message):
        if (len(message) < struct.calcsize(ASF_RMCP_FORMAT)):
            raise "Message too small"
        (self.version, self.reserved, self.seqno, self.message_class,
         self.entreprise_number, self.message_type, self.message_tag,
         self.reserved, data_length) = \
            struct.unpack_from(ASF_RMCP_FORMAT, message)
        if len(message) != data_length + struct.calcsize(ASF_RMCP_FORMAT):
            raise "Bad length"
        rmcp_size = struct.calcsize(ASF_RMCP_FORMAT)
        self.data = bytearray(memoryview(message)[rmcp_size:])

    def to_bytes(self):
        size = struct.calcsize(ASF_RMCP_FORMAT) + len(self.data)
        res = bytearray(size)
        struct.pack_into(ASF_RMCP_FORMAT, res, 0,
                         self.version, self.reserved, self.seqno,
                         self.message_class, self.entreprise_number,
                         self.message_type, self.message_tag, self.reserved,
                         len(self.data))
        memoryview(res)[struct.calcsize(ASF_RMCP_FORMAT):] = self.data
        return res


class PongData:
    def __init__(self, payload):
        if struct.calcsize(ASF_RMCP_PONG_FORMAT) != len(payload):
            print("Bad length for pong payload expected %i but was %i" %
                  (struct.calcsize(ASF_RMCP_PONG_FORMAT), len(payload)))
        (self.entreprise_number, self.oem_defined, self.supported_entities,
         self.supported_interactions, self.reserved1,
         self.reserved2, self.reserved3, self.reserved4, self.reserved5,
         self.reserved6) = struct.unpack_from(ASF_RMCP_PONG_FORMAT, payload)

    def ipmi(self):
        return (self.supported_entities & 127) != 0

    def asf(self):
        return (self.supported_entities & 15) == 1

    def dash(self):
        return (self.supported_interactions & 32) != 0

    def features(self):
        res = []
        if self.ipmi():
            res.append("ipmi")
        if self.asf():
            res.append("asf")
        if self.dash():
            res.append("dash")
        return res

m = Message()
m.message_class = ASF_RMCP_VERSION1
m.message_type = ASF_RMCP_MESSAGE_TYPE_PRESENCE_PING

sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(m.to_bytes(), (address, ASF_RMCP_PORT))
sock.settimeout(1)

logging.info("Listening")
try:
    while True:
        data, addr = sock.recvfrom(1024)
        logging.debug("From " + str(addr[0]) + ": " + str(data))
        if len(data) == 4 and data[0] == ASF_RMCP_VERSION1 and data[2] == 0 \
                and data[3] == ASF_RMCP_MESSAGE_TYPE_PRESENCE_PING_ACK:
            logging.debug("Ack from " + str(addr[0]))
            continue
        try:
            m.load(data)
        except:
            continue
        if m.message_type == ASF_RMCP_MESSAGE_TYPE_PRESENCE_PONG:
            # Pong:
            print(str(addr[0]))
            pongData = PongData(m.data)
            features = pongData.features()
            print("\tEntreprise: %s" %
                  entreprise_name(pongData.entreprise_number))
            if len(features) != 0:
                print("\tFeatures: %s" % ",".join(features))
except socket.timeout:
    pass

Results

We can discover devices on the local network by using its broadcast address:

$ ./rmcp-discover 192.0.2.255
::ffff:192.0.2.56
    Entreprise: Intel
    Features: dash
::ffff:152.81.7.32
    Entreprise: Intel
    Features: dash
::ffff:192.0.2.228
    Entreprise: Intel
    Features: dash
::ffff:192.0.2.230
    Entreprise: Intel
    Features: dash
::ffff:152.81.3.90
    Entreprise: Intel
    Features: dash
::ffff:192.0.2.170
    Entreprise: Intel
    Features: dash
::ffff:152.81.8.105
    Entreprise: Intel
    Features: dash
::ffff:152.81.5.123
    Entreprise: Intel
    Features: dash
::ffff:192.0.2.235
    Entreprise: Intel
    Features: dash
::ffff:192.0.2.29
    Entreprise: Intel
    Features: dash
::ffff:192.0.2.233
    Entreprise: Intel
    Features: dash
::ffff:192.0.2.171
    Entreprise: Intel
    Features: dash

They advertise Intel and DASH: those are probably AMT devices.

We can use the same script to discover IPMI nodes as well:

$ ./rmcp-discover 198.51.100.42
::ffff:198.51.100.42
    Entreprise: Alerting Specifications Forum
    Features: ipmi,asf

We cannot (reliably) use this to detect AMT on the local machine. The reason is that the messages are sent to the ME when they arrive on the hardware Ethernet adapter. Messages emitted by the localhost to its own IP address are handled internally by the OS: they are received by the Ethernet adapter and thus do not reach the ME. In order to communicate to its own ME, the OS needs to communicate using the MEI instead of using IP. The Intel LMS can be installed to reach the local ME over IP: AFAIU, it listens on the suitable TCP and UDP ports and forwards the request to the ME using the MEI.

References

Technical documentation

Documentation

Articles

CVE-2017-5689

Interesting references following the INTEL-SA-00075/CVE-2017-5689 vulerability: