iMessage is a widely used secure messaging app and protocol across the Apple ecosystem. Curious about what it would be like to run iMessage on other platforms, we took a reverse engineering approach to understand how iMessage operates and examine possibilities to extend it to other platforms.

The goal of this article is to show how Apple leverages the fact that it produces the hardware to protect its software. To explore this, we will try to connect via Apple Push Notification (APN) directly on the network level, and see what challenges we face. Along the way, we’ll reverse engineer small parts of the apsd daemon on macOS and the APN protocol itself using popular open-source tools.


Current Solutions

The current de facto solutions for running iMessage outside of the Apple ecosystem require a Mac server and rely on AppleScript scripting to automate UI actions. This eliminates the need to reimplement the message-sending protocol on the client. However, the big tradeoff is that the Mac has to be running as long you want to use iMessage.

Unlike reversing one self-contained binary, iMessage-sending code (like most of internal functions in XNU OSes) goes beyond the scope of and many system daemons are involved in the process, i.e. a microservice architecture, and they rely on XPC messages as an IPC (Inter-Process Communication) mechanism.

Project Zero has already done terrific research on the structure of the daemons involved in iMessage, so I’ll save you all the unnecessary gory details. But in short, once you type a message and press Enter, it travels through multiple processes, namely -> imagent -> identityservicesd -> apsd. I wrote two Frida-based tools to dissect this process and encountered two main challenges.

First, simply looking up ObjC methods statically in the disassembler was too time consuming; there’s a huge number of API calls and layers upon layers for each task. I wrote a simple Objective-C message interceptor, objtree, that logs all messages within the scope of one that I’m interested in. The output is provided in a tree-like fashion. For example, because I know that a certain UI event method triggers message sending, I’d use my tool to hook that method and see all the subsequent ObjC calls laid out beautifully with stack depth-aware formatting. Here’s objtree in action casually dumping more than 3,000 selectors on triggering the keyDown event:

sudo objtree Messages -m "-[NSResponder keyDown:]"

Demonstrate objtree.
objtree in action

Second, after finding the most important ObjC methods, it all comes down to sending an XPC message to some system process/daemon. I wrote another tool for the job, xpcspy, which intercepts XPC messages and enables filtering.

xcpspy intercepting a message to the daemon apsd

In the end, we discover that the daemon apsd is responsible for sending messages through the network. Thanks to Objective-C’s message-dispatching system, searching for selectors with names such as connectTo and send will provide a good, quick idea of where the TCP connection API calls occur.

Show radare2 in action
radare2 searching for Objective-C selectors


Contacting Apple Servers

The APN protocol isn’t new and some research focuses on its security where it’s referenced to as PUSH. Some of the research still holds: the connection is over TLS, port 5223, on domain rand(0,255), and a client certificate is used for authentication on the TLS level.

However, the protocol no longer sends the client certificate on the transport layer via the CertificateRequest and Certificate messages defined in RFC 5246. Instead, APN sends it on the application layer in a connect message/command along with a public token, a nonce and a signature. They are generated in the method -[APSProtocolParser copyConnectMessageWithToken:state:presenceFlags:certificate:nonce:signature:redirectCount:lastConnected:disconnectReason:].

The token parameter is very important because it functions as a user identifier and plays a crucial role in the protocol-protecting mechanism as we shall see later.
Because the APN client certificate is unique to each device and TLS encryption takes place in the application layer, this provides a more secure approach. The transport layer is not encrypted and could leave the certificate out in the open to a man-in-the-middle.
The first point of contact with Apple’s servers happens in -[APSTCPStream _connectToServerWithPeerName:]. In that method, there are TLS session configuration API calls, including private ones such as -[NSURLSessionConfiguration set_socketStreamProperties:] and -[NSURLSessionConfiguration set_tlsTrustPinningPolicyName:]

By the end, the configuration object will look like this:

"_kCFStreamPropertyEnableConnectionStatistics" = 1;
"_kCFStreamPropertyNPNProtocolsAvailable" = (
"_kCFStreamPropertyNoCompanion" = 1;
"_kCFStreamPropertyPrefersNoProxy" = 1;
"_kCFStreamSocketSetNoDelay" = 1;
kCFStreamPropertySSLSettings = {
kCFStreamSSLPeerName = "";
kCFStreamSSLValidatesCertificateChain = 1;

Our goal now is just to have an open connection with Let’s try to open a TLS connection using openssl.

% openssl s_client -connect -quiet
depth=2 O =, OU = incorp. by ref. (limits liab.), OU = (c) 1999 Limited, CN = Certification Authority (2048)
verify return:1
depth=1 C = US, O = "Entrust, Inc.", OU = See, OU = "(c) 2012 Entrust, Inc. - for authorized use only", CN = Entrust Certification Authority - L1K
verify return:1
depth=0 C = US, ST = California, L = Cupertino, O = Apple Inc., CN =
verify return:1
4548513452:error:14020410:SSL routines:CONNECT_CR_SESSION_TICKET:sslv3 alert handshake failure:/AppleInternal/BuildRoot/Library/Caches/ alert number 40
4548513452:error:140200E5:SSL routines:CONNECT_CR_SESSION_TICKET:ssl handshake failure:/AppleInternal/BuildRoot/Library/Caches/

We got a handshake failure. Looking at the _kCFStreamPropertyNPNProtocolsAvailable key above, we see that (NPN) Next Protocol Negotiation is being used.

NPN, now called Application-Layer Protocol Negotiation (ALPN), is a TLS extension embedded in the ClientHello message that tells a TLS server which application layer protocol(s) the client wishes to use. Given the use of extra TLS extensions, it’s wise to record the traffic with tcpdump and examine it. But first, we’ll need to respawn apsd, because the connection happens as it’s starting up. launchctl enables us to terminate then spawn daemons in the debugger:

% sudo launchctl attach -k system/

Now we have apsd stopped at _dyld_start:

Process 1925 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x000000010b447000 dyld` _dyld_start
-> 0x10b447000 <+0>: pop rdi
0x10b447001 <+1>: push 0x0
0x10b447003 <+3>: mov rbp, rsp
0x10b447006 <+6>: and rsp, -0x10
0x10b44700a <+10>: sub rsp, 0x10
0x10b44700e <+14>: mov esi, dword ptr [rbp + 0x8]
0x10b447011 <+17>: lea rdx, [rbp + 0x10]
0x10b447015 <+21>: lea rcx, [rip - 0x101c]
Target 0: (apsd) stopped.​
Executable module set to "/System/Library/PrivateFrameworks/ApplePushService.framework/apsd".
Architecture set to: x86_64h-apple-macosx-.
We'll start the packet recordin, then continue apsd's execution to record the connection:
% sudo tcpdump -i en0 -w /tmp/apsd.pcap
(lldb) c

Now that have a nice traffic dump, let’s check out the handshake:

View of apsd
Traffic capture of apsd

The handshake has some interesting TLS extensions. You can send most of these extensions along with the handshake using the openssl s_client tool, but in my experiments only two are required, besides those that openssl (which is really LibreSSL 2.8.3 at the time of writing) sends by default. Those are the server_name and application_layer_protocol_negotiation extensions. For ALPN, the client sends apns-security-v3 and apns-pack-v1. The server always picked apns-pack-v1 in my experiments. Let’s try to connect to the server using those parameters:

% openssl s_client -connect -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername -quiet
depth=2 C = US, O = Apple Inc., OU = Apple Certification Authority, CN = Apple Root CA
verify error:num=19:self signed certificate in certificate chain
verify return:0

Great, now we’re connected to Apple’s servers! If you omit either one of the -alpn or -servername options, you’ll get a handshake failure. (Also don’t mind the verify error:num=19 this is openssl complaining about the CA certificate, which is naturally self-signed.)


Intercepting APN Messages

Now we need to intercept the unencrypted TLS messages. Certificate pinning used to be relatively easily bypassable on APN. But because bypassing it is a whole different challenge, I’ll resort to intercepting plaintext protocol payload before it leaves the binary, by having breakpoints on the data-sending and receiving methods. Those functions are -[APSTCPStream writeDataInBackground:] and -[APSCourier tcpStream:dataReceived:] respectively.

Process 1958 stopped
frame #0: 0x0000000109a55d83 apsd` ___lldb_unnamed_symbol2607$$apsd
-> 0x109a55d83 <+0>: push rbp
0x109a55d84 <+1>: mov rbp, rsp
0x109a55d87 <+4>: push r15
0x109a55d89 <+6>: push r14
0x109a55d8b <+8>: push rbx
0x109a55d8c <+9>: sub rsp, 0x18
0x109a55d90 <+13>: mov rbx, rdi
0x109a55d93 <+16>: mov rax, qword ptr [rip + 0x924a6] ; (void *)0x00007fff88a98af0: __stack_chk_guard
Target 0: (apsd) stopped.
(lldb) po $rdx
<07ffef04 41203dfe ...lots of redacted data>


rdx holds a reference to the NSData object, whose bytes will be written to the output stream. Same mechanism for receiving data on the input stream.


Communicating with APN

Now that we have the connection and the data, can we communicate via APN? Let’s test it. I used a FIFO for feeding input into openssl.

% mkfifo /tmp/in
% openssl s_client -connect -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername -quiet < /tmp/in > /tmp/out
And for reading response messages enter:
% xxd /tmp/out
00000000: 0822 0180 04a1 1400 0588 0683 08a9 3800 ."............8.
00000010: 0aa5 0176 1474 ee7b 0ca5 0176 1474 ee7b ...v.t.{...v.t.{

Voila! This connect response message has the response code (0x08) and server time, among other parameters.


Dropped Connection

APN is a binary protocol. The commands are serialized in the class APSProtocolParser and the internals of it aren’t what interests us here. This is the smallest sequence of commands possible for sending an iMessage, according to what happens in apsd:

  • 0x07: Connect user with uid 0 (Each user has his own public push token.)
  • 0x0c: Keep alive.
  • 0x14: Active state.
  • 0x07: Connect user with uid 501.
  • 0x09: Filter topics.
  • 0x0a: Send message.

I was able to replicate sending a iMessage purely from openssl by copying binary message data from apsd as it was sending it, and using that as input for my openssl FIFO setup. The most interesting of those commands is filter (0x09). A filter message is serialized in the method -[APSProtocolParser copyFilterMessageWithEnabledHashes:ignoredHashes:opportunisticHashes:nonWakingHashes:pausedHashes:token:]. The hashes in the arguments are for topics, or services that use APN. iMessage has the topic name for some reason. Without the filter message, the client isn’t able to send or receive APN messages via the sendcommand (0x0a). So we must invoke the filter command before sending a message.


Concluding Our Tests

As you saw, replicating APN traffic is easy, but there’s a caveat: the filter command will cause the server to drop any previous connections for the same public token.

Suppose someone had already undergone the pain of reversing the protocol, generated APN-valid messages from scratch, then built a Linux APN client called fakeapsd and copied the connect message parameters (public token & key pair) as-is from a Mac device. The implication of the connection-dropping with the filter command as explained above is that each time fakeapsd tries to have any meaningful communication with the server, it will cause the real apsd’s connection to drop, which in turn will try to reconnect, and it will be a never-ending fight for the connection between fakeapsd and apsd.

Now we mentioned that the server will drop a connection for the same public token, which is the crucial parameter for the connect message. Can a new one be generated to bypass this restriction? Without spending precious time on getting that answer the hard way, we can turn to Hackintosh for the answer.

Hackintosh, which I’m solely considering from a technical standpoint, is the perfect experiment here because all else being equal (protocol-handling and so), it controls one important parameter, and that is running on a real Apple device, which it doesn’t. Getting iMessage & FaceTime to work has been long problematic for many because a serial number has to be brute-forced (the encoding of which has been reversed) until one is found that is genuine, but hasn’t been purchased yet.

As we see, controlling the hardware can be the most essential element in “protecting” a protocol in a white-box scenario in which an adversary has complete access to the software. We also saw how dynamic instrumentation tools such as the NowSecure-sponsored Frida can ease the process of reverse engineering.

To learn more about mobile application security and our research, contact us to speak to an expert.

What to read next:

Abdelrahman Eid

linkedin icon twitter icon

Security Engineer

Abdelrahman is a security engineer for NowSecure. He comes from a programming background and loves low-level stuff. He's reverse engineered some of the most high-profile mobile apps which included state-of-the-art code obfuscation techniques. He's also passionate about cryptography, dynamic instrumentation, binary exploitation, fuzzing and all things mobile app security.