Operationalising Calendar Alerts: Persistence on macOS


Throughout the following blog post we provide insights into calendar alerts, a method of persisting on macOS. Building on the work of Andy Grant over at NCC (https://research.nccgroup.com/2020/05/05/exploring-macos-calendar-alerts-part-1-attempting-to-execute-code/), this post takes deeper look into weaponising the feature for use in offensive operations. This includes reversing Automator.app to find an undocumented API that enables the technique. We’re also releasing the JavaScript for Automation (JXA) code to perform the persistence along with this article, this can be found here, along with a quick getting started guide. However, please read on further if you’re interested in taking a closer look into how this all comes together.


As detailed by Andy, one of the many features of the macOS inbuilt calendar application, is the ability to execute an application as an alert for an event. This can be configured in the GUI when creating a new event as shown below.


This is discussed by Andy in-depth, including research into code execution and data exfiltration (take a look if you haven’t already, it’s an awesome read). However, the part that caught my eye was regarding persistence. He talks about the issues he encountered with attempting to programmatically insert an event to perform this task using AppleScript, and that the calendar application simply ignored his requests to do so. Another route was to modify the SQLite database that powers the app - this is awesome, and lets you set the default alert for all upcoming events as an added bonus. However, this method proved difficult to operationalise.

The thread

Whilst having a play with the technique, I came across Automator.app, Apple’s solution to building drag-and-drop applications that perform repetitive tasks. Included within Automator is a template to build a calendar alarm.


Stepping through the GUI and saving the workflow - results in a new event being added to the calendar, with an alert that executes our workflow as a new application! Alright this is a good step, so it must be possible to do this programmatically.

I started digging into the Apple Developer documentation for EventKit - the framework responsible for interaction with calendar events, but found no mention of the API that would allow me to add the alert that would execute my application. So, I figured it would be worth taking a closer look into Automator to see how it’s achieving this.

I first used Objective See’s FileMonitor and ProcessMonitor to get some insight into what Automator was doing under the hood. It wasn’t modifying the calendar database, nor was it really modifying any other files of interest. So, I then turned to the LLVM debugger, lldb. Pivoting off a few of the EventKit functions, I ended up in the function [AMICalPluginWorkflowPersonality finishSavingWorkflow:forOperation:atURL:error:]. Taking a look at the disassembly of this function shows a series of EventKit functions that look similar to the documentation provided by Apple.

<+735>:  movq   0x58d6ff62(%rip), %rdi    ; (void *)0x00007fff90b872b0: AMEventKitSoftLinking
<+742>:  movq   0x58d6eb8b(%rip), %rsi    ; "EKEvent"
<+749>:  callq  *%r14
<+752>:  movq   0x58d6ec69(%rip), %rsi    ; "eventWithEventStore:"
<+759>:  movq   %rax, %rdi
<+762>:  movq   %r12, %rdx
<+765>:  callq  *%r14
<+768>:  movq   %rax, %rdi
<+771>:  callq  0x7fff37e3e744            ; symbol stub for: objc_retainAutoreleasedReturnValue
<+776>:  movq   %rax, %rbx
<+779>:  movq   0x58d6898e(%rip), %rsi    ; "setTitle:"
<+786>:  movq   %rax, %rdi
<+789>:  movq   -0x98(%rbp), %rdx
<+796>:  callq  *%r14
<+799>:  movq   0x58d6ec42(%rip), %rsi    ; "setStartDate:"
<+806>:  movq   %rbx, %rdi
<+809>:  movq   %r15, %rdx
<+812>:  callq  *%r14
<+815>:  movq   0x58d6ec3a(%rip), %rsi    ; "setEndDate:"
<+822>:  movq   %rbx, %rdi
<+825>:  movq   %r15, -0xd0(%rbp)
<+832>:  movq   %r15, %rdx
<+835>:  callq  *%r14

We can see that the disassembly above creates a new event using the EKEvent class, and sets the title, start and end dates. However, one section wasn’t quite as familiar, and contained an interesting function that I couldn’t find any information about on the Apple docs, [EKAlarm procedureAlarmWithBookmark]. Below is a section of the disassembly leading up to this undocumented function.

<+876>:  movq   0x58d6ec05(%rip), %rsi    ; "bookmarkDataWithOptions:includingResourceValuesForKeys:relativeToURL:error:"
<+883>:  xorl   %r12d, %r12d
<+886>:  movl   $0x200, %edx              ; imm = 0x200 
<+891>:  xorl   %ecx, %ecx
<+893>:  xorl   %r8d, %r8d
<+896>:  movq   %r13, %r9
<+899>:  callq  *0x58cd7f17(%rip)         ; (void *)0x00007fff723f3800: objc_msgSend
<+905>:  movq   %rax, %rdi
<+908>:  callq  0x7fff37e3e744            ; symbol stub for: objc_retainAutoreleasedReturnValue
<+913>:  movq   %rax, %r15
<+916>:  movq   (%r13), %rdi
<+920>:  callq  *0x58cd7f12(%rip)         ; (void *)0x00007fff723f36d0: objc_retain
<+926>:  movq   %rax, %r13
<+929>:  testq  %r15, %r15
<+932>:  je     0x7fff37e1138a            ; <+1034>
<+934>:  movq   0x58d6fe9b(%rip), %rdi    ; (void *)0x00007fff90b872b0: AMEventKitSoftLinking
<+941>:  movq   0x58d6ebcc(%rip), %rsi    ; "EKAlarm"
<+948>:  callq  *%r14
<+951>:  movq   0x58d6ebca(%rip), %rsi    ; "procedureAlarmWithBookmark:"
<+958>:  movq   %rax, %rdi
<+961>:  movq   %r15, %rdx
<+964>:  callq  *%r14

We can see that Automator creates a new bookmark, which is a data structure that points to a specific file on disk. It then creates a new instance of the EKAlarm class, and calls procedureAlarmWithBookmark on that class, with the bookmark data as an argument. Sweet, this was exactly what we were looking for.

Bringing it all together

At this point we have all the pieces to action our persistence programmatically. I’ve put this together in JXA so that it can be easily executed using Mythic (formerly Apfell). The scenario I’m going to walk through below results in a new calendar event series being added to a specific calendar. This isn’t the only (or the stealthiest) way of performing this technique, and the included JXA contains the necessary functionality to backdoor existing events or otherwise manipulate the user’s calendars. Check out the GitHub repository to learn more.

Before we do anything, we need to setup Mythic, and get ourselves an Apfell implant. The Mythic docs are great, and you can follow the steps here to get started. Next, we need to import our functionality into Apfell, using the jsimport command.


At this point, our script is loaded into the Apfell implant and we can start calling functions from it. To kick off the technique, we first need to enumerate the user’s calendars. We use the list_calendars function for this. This will prompt the user to grant permissions to the calendar (and sometimes contacts) for the process executing the code.


We’re going to choose the Automator calendar, and we note down its UID. Next, we use the persist_calalert function included in the JXA to create the new events. The function call looks like this;

"My Event", // Title
"/Users/rookuu/Library/Apfell.app", // Target App
60, // Delay in seconds
"daily", // Frequency of recurrence
1, // Interval of recurrence
3, // Number of events
"711CE045-7778-4633-A6FA-27E18ADD0C17" // UID of the calendar


The process will now create the new events and insert them into the calendar. The “Delay in seconds” argument specifies how far into the future the first event should be, and how often the persistence fires is governed by the frequency, interval and number of events parameters. In this case, we will create a new event every day for 3 days, that when triggered, will launch our malicious application.


Kicking this off in Apfell, and we see the following result in the calendar. When each event starts, the first being at 18:53 09/10/2020, for 3 days, the specified app will execute. In our case we’re executing an Apfell payload as an application, and all the specified times, we receive the connect back from our shell. Persistence achieved!

 Sandboxing on macOS

Whilst preparing this post, Calum Hall (@_chall) rightly pointed out that I’d failed to consider sandboxing, and that even though the app is executed, it’s not going to be useful unless I’m executing code outside of the Calendar sandbox.


To my complete surprise, it turns out that we don’t need to worry about escaping the sandbox, since we were never in it in the first place. Whilst Calendar is a sandboxed process, it seems that the applications executed via alerts are not. In the screenshot above, we can see that our executed appication, CalendarAlarmSandboxTest, does not sit within a sandbox.

A Word on Detection from F-Secure Countercept

Detection of this technique will largely depend on your environment, the telemetry which you have available and the exact way the technique is implemented or executed. 

Anomalous File Access Events

Unusual applications creating or modifying calendar events files under the '/Users/{USER}/Library/Calendars/' path is a key indicator of this technique. In particular, watch out for any scripting processes such as Python, osascript, ruby etc. conducting such file creation or modifications. When the bundled script is used to create or modify an event, it creates or edits ICS files within the previously mentioned path. 

Process Relationships

In addition to the file access events described above, practitioners may also consider hunting for suspicious process relationships. Under usual circumstances you could expect calendar event related process to provide some sort of link between the calendar application itself and the program or payload that is being executed. However, due to the way in which interprocess communication works on macOS the 'launchd' process appears as the parent process for most executions, affecting defenders ability to draw accurate parent/child relationships and process trees. This topic is thoroughly explained in 'The Truetree Concept' post by Jaron Bradley, which also includes information on obtaining "Responsible" and "Submitted by" process identifiers by utilising undocumented APIs and XPC services respectively. In this case, the final "payload" application is submitted by CoreServicesUIA.

Suspicious osascript Processes

Detection of persistence techniques is not always trivial. It can be best to focus detection efforts on earlier stages of the kill chain. The Proof of Concept discussed in this post makes use of Apfell's JXA payload and could be detected by monitoring for osascript processes via ESF. Again, osascript executions are likely to be a common occurrence on a macOS system and across an environment. To increase the fidelity of the detection, the data needs to be enriched, for example by looking for references of the following in command line arguments or functionality:

  • Execution of JavaScript
  • Execution of shell scripts such as "do shell script"
  • Scripts attempting to run with elevated privileges and/or the use of dialog boxes to elicit user input.


Calendar alerts are a neat trick to gain persistence on macOS devices, go have a play with the CalendarPersist.js JXA and Mythic/Apfell. Finding undocumented APIs is a good time, and there is loads more to discover in this area. Any questions, shoot them over to me on Twitter @rookuu_.