Skip navigation
← Back to Index

Deciphering the Messages of Apple’s T2 Coprocessor

by Jeremy Erickson and Misha Davidov

00. Introduction

In 2018, we released two whitepapers exploring Apple’s T2 coprocessor. The first paper explored the new system architecture of the late 2017 iMac Pro and 2018 MacBook Pro and how the inclusion of the T2 coprocessor enabled the secure boot and encrypted storage capabilities of this new platform. The second paper performed a deep-dive into the Secure Boot process and raised the concern that the T2 coprocessor, running a full version of BridgeOS, may expose a large attack surface. In this article, we explore the exposed services, identify the communications transport and decipher the protocols macOS uses to communicate with the T2 coprocessor.

It will shock nobody that the T2 coprocessor communicates with macOS using Apple’s XPC interprocess communication mechanism. However, since the low-level workings of this communication mechanism are documented sparsely or not at all, this article aims to record not only the standard message format, but also how the T2’s use of XPC messaging appears to differ from conventional use of XPC. Building upon this understanding of the low-level communication channel, we demonstrate how one may analyze the network traffic between a macOS client and a T2 server and use this to exercise additional T2 functionality.

Our exploration of Apple’s T2 chip is based on our observations from two late 2017 iMac Pros and a 2018 MacBook Pro.

01. RemoteXPC

XPC is an interprocess communication mechanism designed by Apple which utilizes serialized property lists for messages. Apple has extended this mechanism to support a new range of functionality. Built on top of Network.framework, the RemoteXPC facility allows for XPC message passing between networked endpoints. The T2 coprocessor utilizes RemoteXPC to communicate to the host macOS installation. Currently, T2 services utilize this facility to pass messages back and forth as well as perform more complicated actions such as file transfers between the two domains, but we may very well see RemoteXPC used more broadly for cross-product communication in the future as the foundation is there.

T2 services that wish to utilize this transport register with the subsystem using the RemoteXPC library located at /System/Library/PrivateFrameworks/RemoteXPC.framework/RemoteXPC on macOS and baked into the dyld shared cache on BridgeOS. Services advertise their endpoints by invoking the xpc_remote_connection_create_remote_service_listener function with their reverse domain name notation endpoint identifier (e.g. com.apple.sysdiagnose.remote) that will be used to look up the service and set up an event handler for incoming connections to be routed to. The services then can then parse and reply to incoming messages through the use of strongly typed XPC dictionaries.

02. Exposed T2 Services

Located in /usr/libexec, Apple ships a feature-rich utility to interrogate and interface with the RemoteXPC facility called remotectl:

$ remotectl
usage: remotectl list
usage: remotectl show (name|uuid)
usage: remotectl get-property (name|uuid) [service] property
usage: remotectl dumpstate
usage: remotectl browse
usage: remotectl echo [-v service_version] [-d (name|uuid)]
usage: remotectl eos-echo
usage: remotectl netcat (name|uuid) service
usage: remotectl relay (name|uuid) service
usage: remotectl loopback (attach|connect|detach|suspend|resume)
usage: remotectl convert-bridge-version plist-in-path bin-out-path
usage: remotectl heartbeat (name|uuid)
usage: remotectl trampoline [-2 fd] service_name command args ... [ -- [-2 fd] service_name command args ... ]

A list command will display the connected devices and some basic information about them:

$ /usr/libexec/remotectl list
4525CE68-3808-49CB-89A5-9DAE6E329B39 localbridge      iBridge2,1   J137AP   2.0 (15P2064/15.16.2064.0.0,0)

A show command followed by a device name will display detailed information about the connected device as well as a list of advertised service endpoints:

$ ./remotectl show localbridge
Found localbridge (bridge)
    State: connected (connectable)
    UUID: 8366C34C-12B7-4BAD-9BFB-9896FFEC6A37
    Product Type: iBridge2,3
    OS Build: 2.4.1 (15P6613)
    Messaging Protocol Version: 1
    Heartbeat:
        Last successful heartbeat sent 9.892s ago, received 9.888s ago (took 0.004s)
        504 heartbeats sent, 0 received
    Properties: {
        AppleInternal => false
        ChipID => 32786
        EffectiveProductionStatusSEP => true
        HWModel => J680AP
        HasSEP => true
        LocationID => 2148532224
        RegionInfo => LL/A
        EffectiveSecurityModeAp => true
        FDRSealingStatus => true
        SigningFuse => true
        BuildVersion => 15P6613
        OSVersion => 2.4.1
        BridgeVersion => 15.16.6613.0.0,0
        SensitivePropertiesVisible => true
        ProductType => iBridge2,3
        Image4CryptoHashMethod => sha2-384
        SerialNumber => C02X60YUJGH5
        BootSessionUUID => EF9BCE55-BF25-4ADA-8034-FA46AE6DCEEF
        BoardId => 11
        EffectiveProductionStatusAp => true
        EffectiveSecurityModeSEP => true
        UniqueChipID => 1689691881472038
        UniqueDeviceID => 00008012-000600C40C600026
        RemoteXPCVersionFlags => 72057594037927942
        CertificateSecurityMode => true
        CertificateProductionStatus => true
        ModelNumber => Z0V00007Z
        RegionCode => LL
        InterfaceIndex => 8
        HardwarePlatform => t8012
        Image4Supported => true
    }
    Services:
        com.apple.sysdiagnose.stackshot.remote
        com.apple.eos.BiometricKit
        com.apple.nfcd.relay.control
        com.apple.logd.remote-daemon
        com.apple.CSCRemoteSupportd
        com.apple.aveservice
        com.apple.osanalytics.logTransfer
        com.apple.multiverse.remote.bridgetime
        com.apple.nfcd.relay.uart
        com.apple.private.avvc.xpc.remote
        com.apple.sysdiagnose.remote
        com.apple.corespeech.xpc.remote.control
        com.apple.corespeech.xpc.remote.record
        com.apple.powerchime.remote
        com.apple.xpc.remote.multiboot
        com.apple.bridgeOSUpdated
        com.apple.eos.LASecureIO

Two of the most interesting commands are netcat and relay, which establish a raw connection to a specified service and either redirect stdin/stdout to the connection or fork-off a listen socket for use with third-party applications. Available services vary by platform (i.e. MacBook Pro versus iMac Pro) and cover a wide range of functionality. While the implementation and functionality of these services is outside the scope of this article, we do strongly encourage you to look for yourselves and to share your findings.

Of particular note, remotectl does not require the invoking user to have root privileges to leverage these commands. This means that a non-privileged user may directly interface with T2 services at a very low level and access functionality that might not be exposed at the host macOS. They just need to understand the underlying transport.

03. Communication Transport

To discover and communicate with advertised services, the T2 exposes itself to macOS as a network interface, assigned as en6 on our lab machines. This macOS interface is configured for IPv6 with a universally static local address of fe80::aede:48ff:fe00:1122. The T2 exposes itself at a fixed IPv6 address of fe80::aede:48ff:fe33:4455.

$ ifconfig
...
VHC128: flags=101<UP,PROMISC> mtu 0
...
en6: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether ac:de:48:00:11:22
    inet6 fe80::aede:48ff:fe00:1122%en6 prefixlen 64 scopeid 0x9
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect (100baseTX <full-duplex>)
    status: active

The T2 network interface (en6) is privileged and gated by the com.apple.private.RemoteServiceDiscovery.device-admin and com.apple.private.network.intcoproc.restricted entitlements. Routing and capturing traffic through this interface is not normally possible. However, if SIP is disabled and VHC128 interface is brought up with the ifconfig VHC128 up command, traffic can be captured and analyzed with Wireshark on the VHC128 interface.

Typical raw output from the VHC128 interface

04. Decoding Message Layers

MacOS and the T2 communicate over a typical network stack, with a few notable exceptions. Ethernet frames are encapsulated within Mobile Broadband Interface Model (MBIM) packets for transmission over what we can therefore infer is a USB-based interface. For simple application-level messages, one MBIM frame will typically contain a single message, but for larger data transfers, multiple Ethernet frames will be encapsulated within each packet. This is somewhat ironic, as often a data transfer segment will be split into MTU-sized chunks at the TCP layer, only to be combined into a single packet at the MBIM layer.

Multiple Ethernet frames encapsulated in a single MBIM packet

Above the TCP/IP layer, macOS and the T2 use the HTTP/2 protocol to open, and often maintain, persistent connections between different applications. For the uninitiated, the following is a very cursory description of the protocol:

In HTTP/2, a single persistent connection is formed between two application endpoints. Then, either side can open a stream by sending the other a HEADERS frame specifying the stream ID to open. Stream IDs are monotonically increasing numbers, typically starting at 1, and a stream can be thought of as analogous to an old HTTP/1 connection. Once a stream has been opened, DATA frames can be sent in either direction to pass application data on that stream.

Conventionally, HTTP/2 is used as a way of effectively bundling the multiple HTTP/1 connections two endpoints might form over a short period of time into a single connection to reduce overhead. An HTTP/2-compliant HEADERS frame would also contain standard HTTP header parameters that describe the payload. However, Apple appears to be using the HTTP/2 protocol only as a mechanism to maintain persistent connections, passing empty HEADERS frames to open new streams. This breaks section 8.2.1 of the HTTP/2 specification. We work around this by modifying the h2 module to allow empty HEADERS frames.

Apple’s use of the HTTP/2 protocol is also irregular for two other reasons. First, the HTTP/2 specification mandates that stream IDs must be created in a monotonically increasing order. However, Apple’s application endpoints appear to hardcode a handful of particular stream IDs for certain communications, which when opened do not conform to this monotonic ordering requirement. Second, client-server communication patterns often use multiple streams in unusual ways. For instance, heartbeat messages send requests over stream 1 and responses on stream 3.

Heartbeats on streams 1 and 3

Above the HTTP/2 layer, the observed message format is similar to standard XPC, with a few differences. The largest is the encapsulation of each XPC message within what we call the XPC wrapper, for lack of a better term. Every XPC object is encapsulated within an XPC wrapper. However, it is not uncommon to see XPC headers sent with missing XPC payloads, most likely as control signals. File transfers will send raw data encapsulated directly in the HTTP/2 DATA frame (i.e. no XPC), and an empty HTTP/2 DATA frame appears to always signal the endpoint to close the stream, although this behavior is likely application-specific.

Layers of packet encapsulation

Note: for exceptionally large data payloads, such as file transfers, the data will be split across multiple MBIM packets.

05. XPC Wrapper Fields and Behavior

The XPC wrapper has the following structure, with fields in little-endian order:

Structure of XPC Wrapper


struct XPC_Wrapper
{
    uint32_t magic;
    uint32_t flags;
    uint64_t body_len;
    uint64_t msg_id;
}

Its magic bytes are 0x29B00B92. We have not fully-reverse engineered the flags field, but observed protocols give some indication as to the meaning of certain bits. The body_len field describes the length of the following payload. The msg_id field is used to identify messages. It is used in slightly different ways depending on the protocol. As examples, we will look at the heartbeat and sysdiagnose protocols.

The heartbeat protocol is not exposed as a discrete RemoteXPC service but is implemented at the service discovery layer. Every 20 seconds or so, macOS will send a heartbeat request to the T2 and the T2 will immediately respond with a heartbeat reply. The reply’s msg_id field matches the request’s msg_id and the msg_id increments by 2 for every request/reply pair. The 17th bit of the flags field appears to designate a heartbeat request and the 18th bit of the flags field appears to designate a heartbeat reply.

Heartbeat request:

Flags: 0b 00000000 00000001 00000001 00000001 (0x10101)
MessageId: 0x23f89

Heartbeat reply:

Flags: 0b 00000000 00000010 00000001 00000001 (0x20101)
MessageId: 0x23f89

The sysdiagnose service protocol, exposed as com.apple.sysdiagnose.remote, is more complex and yields more clues about the flags field. The first bit of the flags field appears to always be set. Also, it appears that no other bit in the right-most octet is ever set. This may indicate its use as a protocol version field. The 9th bit appears to indicate that an XPC object will be present in the payload, but is not set when the XPC object contains only an empty dictionary. The 21st and 22nd bits appear to signal the opening of a new stream to perform a file transfer. When a new stream is opened to transfer a file from the T2 to macOS, the T2 will send an XPC wrapper containing the 21st bit and no payload. macOS will then reply with an XPC wrapper containing the 22nd bit and no payload. Then the T2 will send raw data over the HTTP/2 stream until the file has been transferred. The 23rd flags bit appears to be used during an initial handshake at the start of the sysdiagnose protocol. This handshake is initiated on an otherwise-unused HTTP/2 stream in which macOS will send an empty XPC wrapper with this 23rd bit set and the T2 will reply with an identical empty XPC wrapper.

Flag bits:
00000000 00000000 00000000 00000001 - Always set
00000000 00000000 00000001 00000000 - Data present
00000000 00000001 00000000 00000000 - Heartbeat request
00000000 00000010 00000000 00000000 - Heartbeat reply
00000000 00010000 00000000 00000000 - Opening a new file_tx stream
00000000 00100000 00000000 00000000 - Reply from file_tx stream
00000000 01000000 00000000 00000000 - Sysdiagnose init handshake

The msg_id field helps link file transfer metadata to the file transfer data itself. When sending a file, the T2 will send a description of the file on HTTP/2 stream 1 in the XPC payload, including a msg_id to be used in the future. It will then open a new stream for the file transfer and send an empty XPC wrapper with that msg_id before sending the data. The msg_id field also appears to have different semantic meaning depending on the context of the messages sent. For instance, in the sysdiagnose protocol, macOS sends the T2 a message “REQUEST_TYPE=1” with a msg_id of 0x1 and the T2 later replies with a message “RESPONSE_TYPE=1” with a msg_id of 0x2. Other file transfers in the middle of the sysdiagnose protocol, which are part of a different long-running connection, appear to increment the msg_id field by 2 between messages.

Further details of the sysdiagnose protocol will be covered below.

06. XPC Object Decoding

The XPC object format itself is undocumented. Apple publishes an application-level API for XPC messaging, but reserves the right to change the XPC object format at any time. Despite its being the de-facto standard message-passing format on Apple systems, there is relatively little third-party documentation of this object format. Two notable third-party resources were instrumental in learning to parse the XPC object format. The first is Jonathan Levin’s *OS Internals: Vol 1 book, which is an excellent resource for understanding the service and messaging frameworks of Apple’s operating systems. The second was a slide deck from Ian Beer’s talk at the Jailbreak Security Summit.

XPC objects are 4-byte aligned. A magic number and version number compose the XPC header. As mentioned in Levin’s book, the XPC header changes periodically, with previous magic bytes values of 0x58504321 (“XPC!”) and 0x40585043 (“@XPC”). In our testing on 10.13.3 “High Sierra”, we saw magic bytes of 0x42133742 (no longer a string containing “XPC”), followed by a version number 0x5, which interestingly matches the version number in Levin’s book for post-Darwin 16 despite the magic bytes update. These two fields (magic and version number) make up the entirety of the XPC object header. However, if the header is present, it is always followed by a dictionary object that contains any message contents to be transmitted, even if the dictionary is empty.

Structure of XPC Header

XPC objects are always prefixed with a 4-byte type field. What follows the type field is dependent on the type of the XPC object to be encoded. Many objects have similar formats, so rather than provide an exhaustive description of each XPC type, we will explain the formats of several categories of object types. xpc_types.py can be used as a more complete reference, although we have not deciphered the formats of every XPC type.

Types:
XPC_NULL              = 0x00001000
XPC_BOOL              = 0x00002000
XPC_INT64             = 0x00003000
XPC_UINT64            = 0x00004000
XPC_DOUBLE            = 0x00005000
XPC_POINTER           = 0x00006000
XPC_DATE              = 0x00007000
XPC_DATA              = 0x00008000
XPC_STRING            = 0x00009000
XPC_UUID              = 0x0000a000
XPC_FD                = 0x0000b000
XPC_SHMEM             = 0x0000c000
XPC_MACH_SEND         = 0x0000d000
XPC_ARRAY             = 0x0000e000
XPC_DICTIONARY        = 0x0000f000
XPC_ERROR             = 0x00010000
XPC_CONNECTION        = 0x00011000
XPC_ENDPOINT          = 0x00012000
XPC_SERIALIZER        = 0x00013000
XPC_PIPE              = 0x00014000
XPC_MACH_RECV         = 0x00015000
XPC_BUNDLE            = 0x00016000
XPC_SERVICE           = 0x00017000
XPC_SERVICE_INSTANCE  = 0x00018000
XPC_ACTIVITY          = 0x00019000
XPC_FILE_TRANSFER     = 0x0001a000

Fixed-sized objects, such as uint64, tend to have a simple fixed format:

Structure of a fixed size xpc object

Example: a uint64 with value 0x5:

00 40 00 00 05 00 00 00 00 00 00 00
|___type__| |________value________|

The value has a known size, so there is no length field present.

Variable-length objects, such as strings, may also specify a length:

Structure of a variable length xpc object

Strings are null-terminated and also padded to the 4-byte alignment.

Example: a string with value “duolabs!”:

00 90 00 00 09 00 00 00 64 75 6f 6c 61 62 73 21 00 00 00 00
|___type__| |__length_| |d__u__o__l__a__b__s__!_\0_padding|

Note that even though the printable characters alone would fit the 4-byte alignment, the required null terminator requires padding out to 12 bytes.

Compound objects, such as dictionaries, are more complex. A dictionary has the following format:

Structure of a compound xpc object

The length field specifies the size of the dictionary in bytes, excluding the type and length fields, but including the num_entries field.

Note that the string-like dictionary keys do not contain a 4-byte length field, as the string type does. They are still null-terminated and 4-byte aligned.

Example: a dictionary containing two uint64s with values of 0x5 and 0x6 and keys of “five” and “six”:

00 f0 00 00 28 00 00 00 02 00 00 00 ...
|___type__| |__length_| |num_entry|

66 69 76 65 00 00 00 00 00 40 00 00 05 00 00 00 00 00 00 00 ... |f__i__v__e_\0_padding| |_type| |value|

73 69 78 00 00 40 00 00 06 00 00 00 00 00 00 00 |s__i_x_\0| |_type| |value|

In addition to these three general categories of XPC objects, the file_transfer type is particularly noteworthy since it is used extensively in the sysdiagnose client, which we discuss below. The file_transfer object has the format:

Structure of a file transfer xpc object

The file_transfer type has an embedded dictionary with a fixed format. The only two fields of note are the embedded uint64 msg_id and embedded dictionary field “s” corresponding to the file_transfer_size. All other fields appear to be static. The file_transfer object is used to inform macOS that the T2 will subsequently start transferring a file of file_transfer_size bytes on a new stream identified by XPC wrapper parameter msg_id. Note that msg_id does not refer to the HTTP/2 stream ID that will be used.

Example: a file_transfer object specifying a subsequent file transfer of size 0x1b981 (113025 bytes) on a stream identified by message ID 0xe:

00 a0 01 00 0e 00 00 00 00 00 00 00 ...
|___type__| |______message_id_____|

00 f0 00 00 14 00 00 00 01 00 00 00 ... |_type| |_length| |num_entry|

73 00 00 00 00 40 00 00 81 b9 01 00 00 00 00 00 |s_\0_pad_| |_type| |_file_transfer_size|

07. Exploring The Sysdiagnose Server

With our newfound understanding of the communications between the T2 chip and macOS, we wished to not only read messages, but also write messages and interact directly with the T2 chip. During our initial investigation of the XPC message format, we extensively used the sysdiagnose -c command to generate sample network traffic. Consequently, the sysdiagnose server on the T2 made a natural target with which to interact.

Sysdiagnose protocol diagram

The sysdiagnose protocol can be thought of as having three semi-distinct parts. In Phase 1, the macOS sysdiagnose client sends a request to the T2 sysdiagnose server. The T2 chip begins collecting diagnostics and creates a gzipped tar archive. In Phase 2, the T2 chip uses a preexisting connection to the macOS SubmitDiagInfo service to send two metadata files with filenames of the format “stacks-<date>.ips”. Approximately 10 to 15 seconds later (on our lab machine), Phase 3 begins and the T2 chip responds to the macOS sysdiagnose client with the archive, named “bridge_sysdiagnose_<date>_Bridge_OS_Bridge_<build-version>.tar.gz”.

Interestingly, although no data appears to be exchanged over HTTP/2 stream 3 (opened during Phase 1), the T2’s sysdiagnose server will stop responding if it is not opened and an empty XPC wrapper is not sent over the stream. This appears to be entirely superfluous, although it clearly sends some signal to the server.

Sysdiagnose request:

New XPC Packet imac->t2 on HTTP/2 stream 1 TCP port 49155
XPC Wrapper: {
    Magic: 0x29b00b92
    Flags: 0b 00000000 00000000 00000001 00000001 (0x101)
    BodyLength: 0x30
    MessageId: 0x1
}
{
    "REQUEST_TYPE":
        uint64 0x0000000000000001: 1
}

Sysdiagnose response:

New XPC Packet t2->imac on HTTP/2 stream 1 TCP port 49155
XPC Wrapper: {
    Magic: 0x29b00b92
    Flags: 0b 00000000 00000000 00000001 00000001 (0x101)
    BodyLength: 0xc0
    MessageId: 0x2
}
{
    "RESPONSE_TYPE":
        uint64 0x0000000000000001: 1
    "FILE_TX":
        MessageId: 0x5
        File transfer size: 0x00000000005b49d7 5982679
    "FILE_NAME":
        "bridge_sysdiagnose_2019.01.18_16-57-46+0000_Bridge_OS_Bridge_16P375.tar.gz"
}

To implement our own sysdiagnose client, we need a way to connect to and communicate with the T2 chip. As mentioned earlier, the remotectl utility provides a relay command that will open a local port on macOS and forward any traffic directed at it to a service running on the T2. To initiate a new connection with the sysdiagnose server then, we ran: /usr/libexec/remotectl relay localbridge com.apple.sysdiagnose.remote and connected to the newly opened port. Using the twisted framework and h2 library, we were able to write a responsive sysdiagnose client application, generating raw XPC objects as needed using xpc_types.py. After understanding the messaging format and the base sysdiagnose protocol, we turned to better understanding the inputs the sysdiagnose server accepts.

The macOS sysdiagnose client has many different options about what data to collect, and some of these options modify the request made to the T2’s sysdiagnose server.

sudo sysdiagnose -cup

{
    "disableUIFeedback": True
    "shouldRunOSLogArchive": False
    "shouldRunLoggingTasks": False
    "shouldDisplayTarBall": False
    "shouldRunTimeSensitiveTasks": True
    "REQUEST_TYPE": uint64 0x0000000000000001: 1
}

From inspection of the T2’s sysdiagnose server binary, we can collect a list of parameters that the sysdiagnose server will accept:

Parameter Type
getMetrics bool
diagnosticID string
baseDirectory string
rootPath string
archiveName string
embeddedDeviceType string
coSysdiagnose string
generatePlist bool
quickMode bool
shouldDisplayTarBall bool
shouldCreateTarBall bool
shouldRunLoggingTasks bool
shouldRunTimeSensitiveTasks bool
shouldRunOSLogArchive bool
shouldRemoveTemporaryDirectory bool
shouldGetFeedbackData bool
disableStreamTar bool
disableUIfeedback bool
setNoTimeOut bool
pidOrProcess string
capOverride NSData
warnProcWhitelist string

Some of these parameters are self-explanatory, such as archiveName.

Request:

{
    "REQUEST_TYPE":
        uint64 0x0000000000000001: 1
    "archiveName":
        "duolabs"
}

Response:

{
    "RESPONSE_TYPE":
        uint64 0x0000000000000001: 1
    "MSG_TYPE":
        uint64 0x0000000000000002: 2
    "FILE_TX":
        MessageId: 0x58
        File transfer size: 0x00000000004a22b6 4858550
    "FILE_NAME":
        "duolabs.tar.gz"
}

Others are less clear. For instance, setting baseDirectory appears to cause the sysdiagnose server to hang. Passing baseDirectory sets an internal variable on the sysdiagnose server, but it is unclear from our manual analysis how this variable is used.

There are nominally 10 sysdiagnose request types:


switch ( REQUEST_TYPE )
{
    case 1u:
        sd_ops_sysdiagnose(...);
    case 2u:
        sd_ops_stackshot(...);
    case 4u:
        sd_ops_cancel(...);
    case 5u:
        sd_ops_cancelAll(...);
    case 6u:
        sd_ops_userinterrupt(...);
    case 7u:
        sd_ops_statusPoll(...);
    case 8u:
        sd_ops_airdrop(...);
    case 9u:
        sd_ops_watchList(...);
    case 10u:
        sd_ops_deleteArchive(...);

Sadly, most appear to be unimplemented, although their names may hint at Apple’s plans for the future.


void sd_ops_airdrop(...)
{
    if ( (unsigned int)os_log_type_enabled(...) )
    {
        _os_log_impl(..., "Airdrop not implemented", ...);

08. Conclusion

Apple’s T2 chip only continues to demonstrate that Apple is pushing the frontiers of secure computing. However, like much of the *OS ecosystem, the communication channels and XPC object format are opaque and can therefore end up receiving less third-party scrutiny than those of more open platforms. In this work, we aim to shine a light upon macOS and T2 communications. By exploring this facet of the T2 chip’s operations, we intend to contribute to the ongoing dialogue within the security community about how secure boot and trusted hardware play a critical role in providing a foundation for trusted computing. This foundation should continue to be extensively explored if it is to provide the bedrock upon which we build our future.

09. Tooling

Following our mission to democratize security, we are publishing the tooling we produced along the way so that researchers everywhere can take advantage of this work. As far as we are aware, the only other publicly-available tooling to analyze XPC objects is Jonathan Levin’s XPoCe tool.

https://github.com/duo-labs/apple-t2-xpc

Our tooling contributions include:

  • sysdiagnose_client.py - a sysdiagnose client that can be used as a template for building future applications to communicate with the T2 chip
  • sniffer.py - a scapy-based sniffer that reassembles and decodes streams of communications between macOS and the T2. It can be used to sniff the traffic on VHC128 or can read packets from a standard pcap file
  • mbim.py - helper module for decoding the MBIM packet structure and extracting Ethernet frames for further processing
  • xpc_wrapper.py and xpc_types.py - helper modules for encoding and decoding XPC wrappers and XPC objects
  • h2/ - a modified copy of the hyper-h2 library that supports header-less HEADERS frames (which break the HTTP/2 specification)

Below, we demonstrate the usage of the xpc_types, xpc_wrapper, and mbim modules to encode and decode XPC objects. These helper modules are used in the sniffer and the sysdiagnose client, which can serve as working usage references.

Note that xpc_types aims to be as complete as possible, but we were unable to observe many of the XPC types that we datamined, particularly those that are not listed in Apple’s XPC Services API. Some types are implemented by referencing in-memory objects, but untested as we did not observe network traffic using them. However, all the main XPC object types are accounted for.

We can parse an XPC payload and turn it into XPC objects very easily:


$ python3
>>> from xpc_types import *
>>> raw_payload = b"\x42\x37\x13\x42\x05\x00\x00\x00\x00\xf0\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x44\x55\x4f\x00\x00\x90\x00\x00\x04\x00\x00\x00\x64\x75\x6f\x00"
>>> stream = XPCByteStream(raw_payload)
>>> xpc_obj = XPC_Root(stream)
>>> print(xpc_obj)
{
    "DUO":
        "duo"
}

The above example converts raw bytes into an XPCByteStream object which has helper functions to pop fields out of the bytestream. It feeds the stream to the XPC_Root constructor, which decodes the outermost header layer and begins the potentially-recursive decoding process of the inner objects. Any individual object can be decoded as well, if you know the starting type:


>>> raw_payload = b"\x00\xf0\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x6e\x75\x6d\x00\x00\x40\x00\x00\xce\xfa\xee\xff\xc0\xff\xca\xde"
>>> stream = XPCByteStream(raw_payload)
>>> xpc_dict = XPC_Dictionary(stream)
>>> print(xpc_dict)
{
    "num":
        uint64 0xdecaffc0ffeeface: 16053925026108209870
}

We can serialize these objects back out to bytes as well:


>>> from hexdump import hexdump
>>> payload = xpc_obj.to_bytes()
>>> hexdump(payload)
00000000: 42 37 13 42 05 00 00 00  00 F0 00 00 14 00 00 00  B7.B............
00000010: 01 00 00 00 44 55 4F 00  00 90 00 00 04 00 00 00  ....DUO.........
00000020: 64 75 6F 00                                       duo.

Finally, we can generate our own XPC objects from scratch:


>>> new_xpc_object = XPC_Root(
...     XPC_Dictionary({
...         "DUO": XPC_String("duo"),
...         "CISCO": XPC_String("cisco"),
...     }))
>>> print(new_xpc_object)
{
    "DUO":
        "duo"
    "CISCO":
        "cisco"
}
>>> hexdump(new_xpc_object.to_bytes())
00000000: 42 37 13 42 05 00 00 00  00 F0 00 00 2C 00 00 00  B7.B........,...
00000010: 02 00 00 00 44 55 4F 00  00 90 00 00 04 00 00 00  ....DUO.........
00000020: 64 75 6F 00 43 49 53 43  4F 00 00 00 00 90 00 00  duo.CISCO.......
00000030: 06 00 00 00 63 69 73 63  6F 00 00 00              ....cisco...

The constructor for each XPC object accepts either an XPCByteStream, or an appropriate representation for that object type. Note: recursive types, such as Dictionaries, do not convert non-XPC objects into XPC objects, nor do they perform a “deep” validation step of user-supplied values.

Wrapping an XPC object in an XPC wrapper is simply a matter of generating an XPC wrapper and then concatenating the XPC object bytes to it. The XPC wrapper just takes the four struct fields as ordered arguments:


>>> from xpc_wrapper import XpcWrapper
>>> xpc_bytes = new_xpc_object.to_bytes()
>>> magic = XpcWrapper.magic_bytes
>>> flags = 0x101
>>> msg_id = 0xe
>>> wrapper = XpcWrapper(magic, flags, len(xpc_bytes), msg_id)
>>> payload = wrapper.to_bytes() + xpc_bytes

Since the constructor is a bit different, we use a from_bytes classmethod to construct a new XpcWrapper from raw bytes:


>>> data = b'\x92\x0b\xb0\x29\x01\x01\x02\x00\x30\x00\x00\x00\x00\x00\x00\x00\x13\xf8\x0d\x00\x00\x00\x00\x00\x42\x37\x13\x42\x05\x00\x00\x00\x00\xf0\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00'
>>> wrapper, payload = XpcWrapper.from_bytes(data)
>>> stream = XPCByteStream(payload)
>>> xpc_obj = XPC_Root(stream)
>>> print(wrapper)
XPC Wrapper: {
    Magic: 0x29b00b92
    Flags: 0b 00000000 00000010 00000001 00000001 (0x20101)
    BodyLength: 0x30
    MessageId: 0xdf813
}
>>> print(xpc_obj)
{
}

Finally, MBIM packets can be decoded into scapy Ethernet frames as follows:


>>> from scapy.packet import Packet
>>> from mbim import MBIM
>>> pkt = Packet(b'\x00\x01\x20\x01\xe1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5c\x54\x59\x00\x00\x00\x00\x00\x00\x00\x10\x80\x02\x04\x82\x02\x4e\x43\x4d\x48\x0c\x00\x69\x6e\xe1\x00\x0c\x00\x4e\x43\x4d\x30\x2c\x00\x00\x00\x3a\x00\xa7\x00\x00\x00\x00\x00\x12\x0c\xea\x05\xfe\x11\x8b\x02\x00\x00\x00\x00\xd6\x1d\xea\x05\xc2\x23\xea\x05\x00\x00\x00\x00\x00\x00\x00\x00\x39\x42\xac\xde\x48\x00\x11\x22\xac\xde\x48\x33\x44\x55\x86\xdd\x60\x04\xb8\x6a\x00\x71\x06\x40\xfe\x80\x00\x00\x00\x00\x00\x00\xae\xde\x48\xff\xfe\x33\x44\x55\xfe\x80\x00\x00\x00\x00\x00\x00\xae\xde\x48\xff\xfe\x00\x11\x22\xe8\xd2\xc0\x00\x5e\x24\x01\xa6\xbb\x7e\x2a\x22\x80\x18\x08\x00\x85\xfb\x00\x00\x01\x01\x08\x0a\x26\x19\xbc\xfb\x53\x6d\x6c\xb2\x00\x00\x48\x00\x00\x00\x00\x00\x03\x92\x0b\xb0\x29\x01\x01\x02\x00\x30\x00\x00\x00\x00\x00\x00\x00\x8f\xf8\x0d\x00\x00\x00\x00\x00\x42\x37\x13\x42\x05\x00\x00\x00\x00\xf0\x00\x00\x20\x00\x00\x00\x01\x00\x00\x00\x53\x65\x71\x75\x65\x6e\x63\x65\x4e\x75\x6d\x62\x65\x72\x00\x00\x00\x40\x00\x00\x47\xfc\x06\x00\x00\x00\x00\x00')
>>> m = MBIM(pkt)
>>> for eth in m:
...     print(eth.show())
###[ Ethernet ]###
  dst       = ac:de:48:00:11:22
  src       = ac:de:48:33:44:55
  type      = IPv6
###[ IPv6 ]###
     version   = 6
     tc        = 0
     fl        = 309354
     plen      = 113
     nh        = TCP
     hlim      = 64
     src       = fe80::aede:48ff:fe33:4455
     dst       = fe80::aede:48ff:fe00:1122
###[ TCP ]###
        sport     = 59602
        dport     = 49152
        seq       = 1579418022
        ack       = 3145607714
        dataofs   = 8
        reserved  = 0
        flags     = PA
        window    = 2048
        chksum    = 0x85fb
        urgptr    = 0
        options   = [('NOP', None), ('NOP', None), ('Timestamp', (639220987, 1399680178))]
###[ Raw ]###
           load      = '\x00\x00H\x00\x00\x00\x00\x00\x03\x92\x0b\xb0)\x01\x01\x02\x000\x00\x00\x00\x00\x00\x00\x00\x8f\xf8\r\x00\x00\x00\x00\x00B7\x13B\x05\x00\x00\x00\x00\xf0\x00\x00 \x00\x00\x00\x01\x00\x00\x00SequenceNumber\x00\x00\x00@\x00\x00G\xfc\x06\x00\x00\x00\x00\x00'

Since one MBIM packet may contain many Ethernet frames (or none), we use a Python generator to allow looping over the encapsulated frames.